package sgr
import "git.sr.ht/~rj/sgr/internal/palette"
// Color represents a 4-bit "standard" color from the ANSI specification.
// The color can be used to set the foreground or background color for text.
type Color uint8
// The following colors are the standard color provided by the ANSI
// standard.
const (
Black Color = iota
Red
Green
Yellow // Also known as brown. Try BrightYellow.
Blue
Magenta
Cyan
White
Default Color = 9
Gray Color = 51 + iota // Also known as bright black
BrightRed // Also known as pink
BrightGreen
BrightYellow
BrightBlue
BrightMagenta
BrightCyan
BrightWhite
)
// PaletteColor represents an 8-bit color from the ANSI specification.
// The color can be used to set the foreground or background color for text.
type PaletteColor uint8
func (c PaletteColor) RGBA() (r, g, b, a uint32) {
return palette.Palette[int(c)].RGBA()
}
//go:build !windows
package sgr
import (
"os"
)
func enableTerminalProcessing(file *os.File) bool {
return true
}
package sgr
import (
"fmt"
"io"
"os"
"strings"
"sync"
xterm "golang.org/x/term"
)
var (
// Formatter used when ANSI escape sequences are supported.
monoFormatter = Formatter{colorDepth: 1}
defaultFormatter = Formatter{colorDepth: 4}
paletteFormatter = Formatter{colorDepth: 8}
truecolorFormatter = Formatter{colorDepth: 24}
// Memoization for NewFormatter.
newFormatterVal *Formatter
newFormatterOnce sync.Once
)
// A Formatter contains information about the capabilities of the output
// Writer, such as whether or not it supports ANSI escape sequences, and
// which sequences are available.
//
// Methods on this type all support being called with a nil pointer. This
// state suppresses all escape sequences, and, in some cases, allows
// optimizations where strings are passed through directly.
type Formatter struct {
colorDepth int
}
// NewFormatter returns a formatter suitable for use with Stdout.
func NewFormatter() *Formatter {
newFormatterOnce.Do(func() {
newFormatterVal = NewFormatterForFile(os.Stdout)
})
return newFormatterVal
}
// NewFormatterForFile returns a formatter suitable for use with
// the file.
func NewFormatterForFile(file *os.File) *Formatter {
term := os.Getenv("TERM")
if term == "dumb" || !xterm.IsTerminal(int(file.Fd())) {
return nil
}
// Platform hook to enable ANSI characters. This exists mainly to support
// windows, where processing of ANSI escape codes is not enabled by default.
if !enableTerminalProcessing(file) {
return nil
}
// https://no-color.org/
if _, nocolor := os.LookupEnv("NO_COLOR"); nocolor {
return &monoFormatter
}
// The environment variable COLORTERM overrides the color depth
// inferred from TERM. Check for a value indicating color support.
if colorterm := os.Getenv("COLORTERM"); colorterm == "truecolor" || colorterm == "24bit" {
return &truecolorFormatter
} else if colorterm == "yes" || colorterm == "true" {
return &paletteFormatter
}
return NewFormatterForTerm(term)
}
// NewFormatterForWriter returns a formatter suitable for use with
// the writer.
func NewFormatterForWriter(w io.Writer) *Formatter {
file, ok := w.(*os.File)
if !ok {
return nil
}
return NewFormatterForFile(file)
}
// NewFormatterForTerm returns a formatter with the correct color depth based
// on the terminal identification string.
func NewFormatterForTerm(term string) *Formatter {
if strings.HasSuffix(term, "-m") {
return &monoFormatter
}
if strings.HasSuffix(term, "-256color") {
return &paletteFormatter
}
if strings.HasSuffix(term, "-truecolor") || strings.HasSuffix(term, "-24bit") {
return &truecolorFormatter
}
return &defaultFormatter
}
// NewFormatterWithANSI returns a formatter that will always use ANSI escape
// sequences. This function exists for testing, and should not be used in
// normal code.
func NewFormatterWithANSI() *Formatter {
return &defaultFormatter
}
// ColorDepth returns the bit-depth used by the terminal to represent colors.
func (f *Formatter) ColorDepth() int {
if f == nil {
return 0
}
return f.colorDepth
}
// Boldf returns an object which will show the formatted text in bold when
// printed using a function in package fmt.
func (f *Formatter) Boldf(format string, a ...interface{}) interface{} {
if f == nil {
return fmt.Sprintf(format, a...)
}
return &formatter{
prefix: sgrBold,
format: format,
args: a,
}
}
// Dimf returns an object which will show the formatted text dimmed when
// printed using a function in package fmt.
func (f *Formatter) Dimf(format string, a ...interface{}) interface{} {
if f == nil {
return fmt.Sprintf(format, a...)
}
return &formatter{
prefix: sgrDim,
format: format,
args: a,
}
}
// Italicf returns an object which will show the formatted text in italic when
// printed using a function in package fmt.
func (f *Formatter) Italicf(format string, a ...interface{}) interface{} {
if f == nil {
return fmt.Sprintf(format, a...)
}
return &formatter{
prefix: sgrItalic,
format: format,
args: a,
}
}
// Redf returns an object which will show the formatted text in red when
// printed using a function in package fmt.
func (f *Formatter) Redf(format string, a ...interface{}) interface{} {
if f == nil {
return fmt.Sprintf(format, a...)
}
return &formatter{
prefix: sgrRedFG,
format: format,
args: a,
}
}
// Greenf returns an object which will show the formatted text in green when
// printed using a function in package fmt.
func (f *Formatter) Greenf(format string, a ...interface{}) interface{} {
if f == nil {
return fmt.Sprintf(format, a...)
}
return &formatter{
prefix: sgrGreenFG,
format: format,
args: a,
}
}
// Style returns an object which will show the text in bold when printed using
// a function in package fmt.
func (f *Formatter) Style(style Style, text string) interface{} {
if f == nil {
return text
}
return &styleFormatter{
prefix: []byte(style),
text: text,
}
}
type styleFormatter struct {
prefix []byte
text string
}
func (s *styleFormatter) Format(f fmt.State, v rune) {
format(
f, v,
s.prefix,
s.text,
)
}
// Stylef returns an object which will show the formatted text in the requested
// style when printed using a function in package fmt.
func (f *Formatter) Stylef(style Style, format string, a ...interface{}) interface{} {
if f == nil {
return fmt.Sprintf(format, a...)
}
return &formatter{
prefix: []byte(style),
format: format,
args: a,
}
}
type formatter struct {
prefix []byte
format string
args []interface{}
}
func (s *formatter) Format(f fmt.State, v rune) {
// Check that the verb is supported. Otherwise, print error message.
if v != 's' && v != 'v' {
// Match the error reporting from the fmt package
fmt.Fprintf(f, "%%!%c(format string=%v)", v, s.format)
return
}
_, _ = f.Write(s.prefix)
if _, ok := f.Width(); ok {
_, _ = io.WriteString(f, "%!(BADWIDTH)")
} else if _, ok = f.Precision(); ok {
_, _ = io.WriteString(f, "%!(BADPREC)")
}
_, _ = fmt.Fprintf(f, s.format, s.args...)
_, _ = f.Write(sgrReset)
}
// Package pad writes padding to memory buffers.
//
// Although the functions take an io.Writer, the writers must be memory
// buffers and never return an error.
package pad
import "io"
// WritePadding writes count bytes from the fill, repeating fill as necessary.
func WritePadding(w io.Writer, count int, fill string) {
fl := len(fill)
for fl < count {
_, _ = io.WriteString(w, fill)
count -= fl
}
_, _ = io.WriteString(w, fill[:count])
}
// WriteSpaces writes count spaces.
func WriteSpaces(w io.Writer, count int) {
WritePadding(w, count, " ")
}
// Package palette provides mapping for 24-bit colors to the 8-bit ANSI SGR
// palette.
package palette
var (
// Palette is the 256-color lookup table defined by ANSI for use with
// SGR escape codes.
Palette = [0x100]RGB{
{0, 0, 0},
{192, 0, 0},
{0, 192, 0},
{192, 192, 0},
{0, 0, 192},
{192, 0, 192},
{0, 192, 192},
{192, 192, 192},
{128, 128, 128},
{255, 0, 0},
{0, 255, 0},
{255, 255, 0},
{0, 0, 255},
{255, 0, 255},
{0, 255, 255},
{255, 255, 255},
}
)
func init() {
// Initialize colors from index 16 to 231. These are a 6x6x6 cubee.
for i := 16; i < 232; i++ {
r := (i - 16) / 36
g := ((i - 16) / 6) % 6
b := (i - 16) % 6
Palette[i].R = uint8((r * 255) / 5)
Palette[i].G = uint8((g * 255) / 5)
Palette[i].B = uint8((b * 255) / 5)
}
// Initialize colors from inidex 232 to 255. These are grays in 24 steps.
for i := 0; i < 24; i++ {
g := uint8((i * 255) / 23)
Palette[i+232] = RGB{g, g, g}
}
}
// FindSimpleColor matches the r, g, b, color to one of the first 16 entries.
// The first 16 entries match the colors available in the 4-bit palette.
func FindSimpleColor(r, g, b uint8) int {
color := 0
dist := calculateDistance(RGB{r, g, b}, RGB{})
for i := 1; i < 16; i++ {
newDist := calculateDistance(RGB{r, g, b}, Palette[i])
if newDist < dist {
color = i
dist = newDist
}
}
return color
}
// FindPaletteColor matches the r, g, b, color to the closest color in the
// 8-bit palette. The first 16 colors are not used, as the exact color for
// those entries are terminal dependent, and matching cannot be guarantted.
func FindPaletteColor(r, g, b uint8) int {
color := 16
dist := calculateDistance(RGB{r, g, b}, RGB{})
// Avoid using the first 16 colors from the palette. Terminals
// disagree about the exact RGB for those colors, and the values in the
// palette are just approximations.
for i := 17; i < 0x100; i++ {
newDist := calculateDistance(RGB{r, g, b}, Palette[i])
if newDist < dist {
color = i
dist = newDist
}
}
return color
}
func calculateDistance(a, b RGB) int32 {
return sq(a.R, b.R) + sq(a.G, b.G) + sq(a.B, b.B)
}
func sq(a, b uint8) int32 {
delta := int32(a) - int32(b)
return delta * delta
}
package palette
import "fmt"
// RGB represents a traditional 24-bit color, having 8
// bits for each of red, green, and blue.
type RGB struct {
R, G, B uint8
}
func (c RGB) RGBA() (r, g, b, a uint32) {
r = uint32(c.R)
r |= r << 8
g = uint32(c.G)
g |= g << 8
b = uint32(c.B)
b |= b << 8
return r, g, b, 0xFFFF
}
func (c RGB) String() string {
return fmt.Sprintf("#%02x%02x%02x", c.R, c.G, c.B)
}
package plot
import (
"fmt"
"io"
"git.sr.ht/~rj/sgr"
"git.sr.ht/~rj/sgr/internal/pad"
)
// HorizontalBar returns an object which will show a horizontal bar when
// printed using a function in package fmt. The parameter value must be
// in the range 0 to 1 inclusive, and sets the percentage of the bar that
// will be filled.
//
// To set the total width of the bar, use the width specifier in the format
// string.
func HorizontalBar(f *sgr.Formatter, value float64, color sgr.Color) interface{} {
// Select options that will work when there is no ANSI support.
if f == nil {
return &hbar{
value: value,
}
}
// We can use full block (U+2588) to print, but that also imposes a
// requirement for unicode support. Unicode support is very likely,
// but still an additional requirement. Handle printing entirely
// by manipulating the foreground and background colors.
if color == sgr.Default || f.ColorDepth() <= 1 {
return &hbar{
value: value,
style: f.NewStyle(sgr.Reverse),
}
}
return &hbar{
value: value,
style: f.NewStyle(sgr.FG(color), sgr.BG(color)),
}
}
type hbar struct {
value float64
style sgr.Style
}
func (hb *hbar) Format(f fmt.State, v rune) {
// Get the width for the bar from the format state.
// Apply a default if the user has not specified a width.
width, ok := f.Width()
if !ok {
width = 10
}
// Determine how many characters to fill, and how many to pad.
midpoint := mulRound(width, hb.value)
// If the caller did not provide a value between 0 and 1, the midpoint
// may not be a valid value. Although the standard library often panics
// on programming bugs, the package fmt takes a different approach.
if midpoint < 0 || midpoint > width {
fmt.Fprintf(f, "%%!(BADVALUE)%g", hb.value)
return
}
// Note: All of the following write to the internal buffer that is part
// of fmt.State. No need to check for errors.
if len(hb.style) > 0 {
_, _ = f.Write(hb.style)
pad.WriteSpaces(f, midpoint)
_, _ = io.WriteString(f, "\x1b[0m" /* reset */)
pad.WriteSpaces(f, width-midpoint)
} else {
const fillSolid = "################################"
pad.WritePadding(f, midpoint, fillSolid)
pad.WriteSpaces(f, width-midpoint)
}
}
//go:build go1.10
// +build go1.10
package plot
import "math"
func mulRound(width int, fraction float64) int {
// math.Round introduced in Go v1.10
return int(math.Round(float64(width) * fraction))
}
package plot
import (
"fmt"
"io"
"git.sr.ht/~rj/sgr"
"git.sr.ht/~rj/sgr/internal/pad"
)
// ProgressBar returns an object which will show a progress bar when
// printed using a function in package fmt. The parameter value must be
// in the range 0 to 1 inclusive, and sets the percentage of the bar that
// will be filled.
//
// To set the total width of the bar, use the width specifier in the format
// string.
func ProgressBar(f *sgr.Formatter, value float64, fg, bg sgr.Color) interface{} {
// Select options that will work when there is no ANSI support.
if f == nil {
return &pbar{
value: value,
}
}
return &pbar{
value: value,
style: f.NewStyle(sgr.FG(fg), sgr.BG(bg)),
}
}
type pbar struct {
value float64
style sgr.Style
}
func (pb *pbar) Format(f fmt.State, v rune) {
// Get the width for the bar from the format state.
// Apply a default if the user has not specified a width.
width, ok := f.Width()
if !ok {
width = 10
}
// Determine how many characters to fill, and how many to pad.
midpoint := mulRound(width, pb.value)
// If the caller did not provide a value between 0 and 1, the midpoint
// may not be a valid value. Although the standard library often panics
// on programming bugs, the package fmt takes a different approach.
if midpoint < 0 || midpoint > width {
fmt.Fprintf(f, "%%!(BADVALUE)%g", pb.value)
return
}
// Note: All of the following write to the internal buffer that is part
// of fmt.State. No need to check for errors.
if len(pb.style) > 0 {
_, _ = f.Write(pb.style)
}
if midpoint > 1 {
if haveUTF8 && len(pb.style) > 0 {
pad.WritePadding(f, (midpoint-1)*len("≡"),
"≡≡≡≡≡≡≡≡≡≡≡≡≡≡")
_, _ = io.WriteString(f, "\u25B6")
} else {
pad.WritePadding(f, midpoint-1, "================")
_, _ = io.WriteString(f, ">")
}
}
pad.WriteSpaces(f, width-midpoint)
if len(pb.style) > 0 {
_, _ = io.WriteString(f, "\x1b[0m" /* reset */)
}
}
package plot
import (
"os"
"strings"
)
var (
haveUTF8 = strings.HasSuffix(os.Getenv("LANG"), "UTF-8")
)
// OverrideHaveUTF8 overrides the package's decision about whether the terminal
// supports unicode. Unicode support is checked by inspecting the environment
// variable LANG. Users should not typically call this function except for
// testing.
func OverrideHaveUTF8(v bool) {
haveUTF8 = v
}
package sgr
import (
"fmt"
"io"
"git.sr.ht/~rj/sgr/internal/pad"
)
var (
sgrReset = []byte("\x1b[0m")
sgrBold = []byte("\x1b[1m")
sgrDim = []byte("\x1b[2m")
sgrItalic = []byte("\x1b[3m")
sgrRedFG = []byte("\x1b[31m")
sgrGreenFG = []byte("\x1b[32m")
)
// Bold returns an object which will show the text in bold when printed using
// a function in package fmt.
func (f *Formatter) Bold(text string) interface{} {
if f == nil {
return text
}
return boldFormatter(text)
}
type boldFormatter string
func (s boldFormatter) Format(f fmt.State, v rune) {
format(
f, v,
sgrBold,
string(s),
)
}
// Italic returns an object which will show the text in italic when printed
// using a function in package fmt.
func (f *Formatter) Italic(text string) interface{} {
if f == nil {
return text
}
return italicFormatter(text)
}
type italicFormatter string
func (s italicFormatter) Format(f fmt.State, v rune) {
format(
f, v,
sgrItalic,
string(s),
)
}
// Dim returns an object which will show the text dimmed when printed using
// a function in package fmt.
func (f *Formatter) Dim(text string) interface{} {
if f == nil {
return text
}
return dimFormatter(text)
}
type dimFormatter string
func (s dimFormatter) Format(f fmt.State, v rune) {
format(
f, v,
sgrDim,
string(s),
)
}
// Red returns an object which will show the text in red when printed using
// a function in package fmt.
func (f *Formatter) Red(text string) interface{} {
if f == nil || f.colorDepth <= 1 {
return text
}
return redFormatter(text)
}
type redFormatter string
func (s redFormatter) Format(f fmt.State, v rune) {
format(
f, v,
sgrRedFG,
string(s),
)
}
// Green returns an object which will show the text in green when printed using
// a function in package fmt.
func (f *Formatter) Green(text string) interface{} {
if f == nil || f.colorDepth <= 1 {
return text
}
return greenFormatter(text)
}
type greenFormatter string
func (s greenFormatter) Format(f fmt.State, v rune) {
format(
f, v,
sgrGreenFG,
string(s),
)
}
func format(f fmt.State, v rune, sgr []byte, s string) {
// Check that the verb is supported. Otherwise, print error message.
if v != 's' && v != 'v' {
// Match the error reporting from the fmt package.
// Note, this function is not always called from sgr.formatter,
// but that is an internal detail. Since all invocations support
// the same verbs, this should be sufficient.
fmt.Fprintf(f, "%%!%c(sgr.formatter=%v)", v, s)
return
}
if prec, ok := f.Precision(); ok && len(s) > prec {
s = s[:prec]
}
// We are writing to a memory buffer. The following calls to io.Write will
// not return an error.
if width, ok := f.Width(); ok && len(s) < width {
if f.Flag('-') {
_, _ = f.Write(sgr)
_, _ = io.WriteString(f, s)
pad.WriteSpaces(f, width-len(s))
_, _ = f.Write(sgrReset)
} else {
_, _ = f.Write(sgr)
pad.WriteSpaces(f, width-len(s))
_, _ = io.WriteString(f, s)
_, _ = f.Write(sgrReset)
}
} else {
_, _ = f.Write(sgr)
_, _ = io.WriteString(f, s)
_, _ = f.Write(sgrReset)
}
}
package sgr
import "git.sr.ht/~rj/sgr/internal/palette"
// Style represents a set of display attributes to use when printing text.
// A style can combine attributes such as whether the text is bold or dimmed,
// as well as setting foreground and background colors.
//
// A Style created by a Formatter should not be used with different Formatters.
// The style encodes parameters that may be specific to the terminal's
// capabilities.
type Style []byte
// NewStyle creates a new style by combining the specified display attributes.
// Display attributes can alter formatting, like bold and italic, or specify
// colors.
//
// Not all terminals will support all attributes. In particular, colors will
// be mapped to lower color-width values to match the terminal's support.
func (f *Formatter) NewStyle(ops ...StyleOption) Style {
// If the formatter is nil, then don't create any escape sequences.
if f == nil {
return nil
}
// If no options are provided, return the escape sequence for a reset.
if len(ops) == 0 {
return sgrReset
}
if f.colorDepth <= 1 {
ops = removeColorOptions(ops)
if len(ops) == 0 {
return nil
}
} else if f.colorDepth <= 4 {
ops = filterColorOptionsTo4(ops)
} else if f.colorDepth <= 8 {
ops = filterColorOptionsTo8(ops)
}
sb := make([]byte, 0, 16)
// Prefix for the escape sequence
sb = append(sb, "\x1b["...)
// Apply first option
sb = writeCommands(sb, ops[0])
// Apply rest of options
for _, v := range ops[1:] {
sb = append(sb, ';')
sb = writeCommands(sb, v)
}
// Suffix to terminate the escape sequence
sb = append(sb, 'm')
return (Style)(sb)
}
func removeColorOptions(ops []StyleOption) []StyleOption {
n := 0
for ; n < len(ops) && !ops[n].isColor(); n++ { //nolint:revive // bug in revive
}
if n >= len(ops) {
return ops
}
for _, v := range ops[n+1:] {
if !v.isColor() {
ops[n] = v
n++
}
}
return ops[:n]
}
func filterColorOptionsTo4(ops []StyleOption) []StyleOption {
for i, v := range ops {
switch v & 0xFF {
case 0x80:
clr := v.palette()
if clr > 15 {
rgb := palette.Palette[clr]
clr = palette.FindSimpleColor(rgb.R, rgb.G, rgb.B)
}
ops[i] = FG(adjustSimpleColor(clr))
case 0x81:
clr := v.palette()
if clr > 15 {
rgb := palette.Palette[clr]
clr = palette.FindSimpleColor(rgb.R, rgb.G, rgb.B)
}
ops[i] = BG(adjustSimpleColor(clr))
case 0x82:
r, g, b := v.rgb()
ops[i] = FG(adjustSimpleColor(palette.FindSimpleColor(r, g, b)))
case 0x83:
r, g, b := v.rgb()
ops[i] = BG(adjustSimpleColor(palette.FindSimpleColor(r, g, b)))
}
}
return ops
}
func adjustSimpleColor(clr int) Color {
if clr >= 8 {
clr = clr + int(Gray) - 8
}
return Color(clr)
}
func filterColorOptionsTo8(ops []StyleOption) []StyleOption {
for i, v := range ops {
switch v & 0xFF {
case 0x82:
r, g, b := v.rgb()
ops[i] = FG8(PaletteColor(palette.FindPaletteColor(r, g, b)))
case 0x83:
r, g, b := v.rgb()
ops[i] = BG8(PaletteColor(palette.FindPaletteColor(r, g, b)))
}
}
return ops
}
func writeCommands(sb []byte, op StyleOption) []byte {
// The standard escape codes are all 7-bit value.
// We still need to adjust depending on the number of decimal digits.
code := uint8(op)
if code < 10 {
return append(sb, '0'+code)
}
if code < 100 {
return append(sb, '0'+(code/10), '0'+(code%10))
}
if code < 0x80 {
return append(sb, '1', '0'+((code/10)%10), '0'+(code%10))
}
// The following codes are not in the ANSI standard, but are used by this
// package to encode color commands that require multiple codes.
if code == 0x80 {
sb = append(sb, "38;5;"...)
return writeCommand(sb, byte(op>>8))
}
if code == 0x81 {
sb = append(sb, "48;5;"...)
return writeCommand(sb, byte(op>>8))
}
if code == 0x82 {
sb = append(sb, "38;2;"...)
sb = writeCommand(sb, byte(op>>8))
sb = append(sb, ';')
sb = writeCommand(sb, byte(op>>16))
sb = append(sb, ';')
sb = writeCommand(sb, byte(op>>24))
return sb
}
if code == 0x83 {
sb = append(sb, "48;2;"...)
sb = writeCommand(sb, byte(op>>8))
sb = append(sb, ';')
sb = writeCommand(sb, byte(op>>16))
sb = append(sb, ';')
sb = writeCommand(sb, byte(op>>24))
return sb
}
return sb
}
func writeCommand(sb []byte, op byte) []byte {
if op < 10 {
return append(sb, '0'+op)
}
if op < 100 {
return append(sb, '0'+(op/10), '0'+(op%10))
}
return append(sb, '0'+(op/100), '0'+((op/10)%10), '0'+(op%10))
}
// StyleOption encodes a single display attribute.
type StyleOption uint32
// Display attributes available for use with NewStyle.
const (
Bold StyleOption = 1 // Set the style to bold.
Dim StyleOption = 2 // Set the style to dim.
Italic StyleOption = 3 // Set the style to italic.
Underline StyleOption = 4 // Set the style to underline.
Blink StyleOption = 5 // Set the style to blink.
Reverse StyleOption = 7 // Set the style to reverse video.
CrossedOut StyleOption = 9 // Set the style to crossed-out.
)
// FG creates a StyleOption that will set the foreground color to one of the
// colors in the 4-bit palette.
func FG(clr Color) StyleOption {
return StyleOption(clr + 30)
}
// FG8 creates a StyleOption that will set the foreground color to one of the
// colors in the 8-bit palette.
func FG8(index PaletteColor) StyleOption {
return 0x80 | (StyleOption(index) << 8)
}
// FG24 creates a StyleOption that will set the foreground color.
func FG24(r, g, b uint8) StyleOption {
return 0x82 |
(StyleOption(r) << 8) |
(StyleOption(g) << 16) |
(StyleOption(b) << 24)
}
// BG creates a StyleOption that will set the background color to one of the
// colors in the 4-bit palette.
func BG(clr Color) StyleOption {
return StyleOption(clr + 40)
}
// BG8 creates a StyleOption that will set the background color to one of the
// colors in the 8-bit palette.
func BG8(index PaletteColor) StyleOption {
return 0x81 | StyleOption(index)<<8
}
// BG24 creates a StyleOption that will set the background color.
func BG24(r, g, b uint8) StyleOption {
return 0x83 |
(StyleOption(r) << 8) |
(StyleOption(g) << 16) |
(StyleOption(b) << 24)
}
func (op StyleOption) isColor() bool {
i := byte(op)
return (i >= 30 && i <= 49) || (i >= 90 && i <= 107) || (i&0x80) != 0
}
func (op StyleOption) palette() int {
return int((op >> 8) & 0xff)
}
func (op StyleOption) rgb() (r, g, b uint8) {
r = uint8(op >> 8)
g = uint8(op >> 16)
b = uint8(op >> 24)
return r, g, b
}
package termimage
import (
"bytes"
"errors"
"fmt"
"image"
"image/draw"
"io"
"os"
"git.sr.ht/~rj/sgr"
"golang.org/x/term"
)
var (
ErrWidthTooSmall = errors.New("terminal width is too small for image")
)
func DrawImage(w io.Writer, f *sgr.Formatter, src image.Image) error {
// Check width of image against width of the terminal
width := getSize(w)
if src.Bounds().Dx() > width {
return ErrWidthTooSmall
}
if gray, ok := src.(*image.Gray); ok {
return drawImageGray(w, gray)
}
if alpha, ok := src.(*image.Alpha); ok {
return drawImageAlpha(w, alpha)
}
if f == nil || f.ColorDepth() <= 1 {
gray := image.NewGray(src.Bounds())
draw.Draw(gray, gray.Rect, src, gray.Rect.Min, draw.Src)
return drawImageGray(w, gray)
}
buf := &bytes.Buffer{}
rect := src.Bounds()
for y := rect.Min.Y; y < rect.Max.Y; y += 2 {
for x := rect.Min.X; x < rect.Max.X; x++ {
r, g, b, _ := src.At(x, y).RGBA()
if y < rect.Max.Y-1 {
r2, g2, b2, _ := src.At(x, y+1).RGBA()
fmt.Fprint(buf,
f.Style(f.NewStyle(
sgr.FG24(uint8(r>>8), uint8(g>>8), uint8(b>>8)),
sgr.BG24(uint8(r2>>8), uint8(g2>>8), uint8(b2>>8))),
"\u2580"),
)
} else {
fmt.Fprint(buf,
f.Style(f.NewStyle(
sgr.FG24(uint8(r>>8), uint8(g>>8), uint8(b>>8)),
sgr.BG(sgr.Black)),
"\u2580"),
)
}
}
buf.WriteByte('\n')
}
buf.WriteString("\x1b[0m")
_, err := buf.WriteTo(w)
return err
}
func drawImageAlpha(w io.Writer, src *image.Alpha) error {
const asciiArt = ".++#"
width, height := src.Rect.Dx(), src.Rect.Dy()
buf := make([]byte, 0, height*(width+1))
for y := src.Rect.Min.Y; y < src.Rect.Max.Y; y++ {
for x := src.Rect.Min.X; x < src.Rect.Max.X; x++ {
a := src.AlphaAt(x, y).A
buf = append(buf, asciiArt[a>>6])
}
buf = append(buf, '\n')
}
_, err := w.Write(buf)
return err
}
func drawImageGray(w io.Writer, src *image.Gray) error {
const asciiArt = ".++#"
width, height := src.Rect.Dx(), src.Rect.Dy()
buf := make([]byte, 0, height*(width+1))
for y := src.Rect.Min.Y; y < src.Rect.Max.Y; y++ {
for x := src.Rect.Min.X; x < src.Rect.Max.X; x++ {
a := src.GrayAt(x, y).Y
buf = append(buf, asciiArt[a>>6])
}
buf = append(buf, '\n')
}
_, err := w.Write(buf)
return err
}
func getSize(w io.Writer) int {
const defaultWidth = 255
if file, ok := w.(*os.File); ok {
width, _, err := term.GetSize(int(file.Fd()))
if err != nil {
return defaultWidth
}
return width
}
return defaultWidth
}
package wcwidth
import (
"strings"
"unicode"
)
// ParagraphLineBreak splits the text such that the first component will occupy
// no more than maxlen columns. Leading spaces are trimmed from lines.
func ParagraphLineBreak(maxlen int, text string) (string, int, string) {
text = strings.TrimSpace(text)
width := 0
for i, r := range text {
if unicode.IsSpace(r) {
return lineBreak(maxlen, text, i, width)
}
width += RuneWidth(r)
}
return text, width, ""
}
func lineBreak(maxlen int, text string, offset, width int) (string, int, string) {
lastOffset := offset
lastWidth := width
for i, r := range text[offset:] {
// If this is a space, then we can advance so that the previous rune
// in included in the cut.
if unicode.IsSpace(r) {
lastOffset = offset + i
lastWidth = width
}
// Advance the width to include the current rune
width += RuneWidth(r)
// Check if we are too wide.
if width > maxlen {
return strings.TrimSpace(text[:lastOffset]),
lastWidth,
strings.TrimSpace(text[lastOffset:])
}
}
return text, width, ""
}
package wcwidth
import "unicode"
// RuneWidth return the number of columns required when printing the rune to a
// terminal.
//
// If the rune is the null character, this functionos returns zero.
//
// If the rune is a control character, this functions a non-specified value.
func RuneWidth(r rune) int {
// Null character is a special case.
if r == 0 {
return 0
}
// Control characters (class Cc) have width -1. Why?
if unicode.Is(unicode.Cc, r) {
return -1
}
// No width for categories Me (Mark, enclosing), Mn (Mark, non-spacing), and
// Cf (Other, format).
if unicode.In(r, unicode.Me, unicode.Mn, unicode.Cf) {
if r == 0xAD /* soft hyphen */ {
return 1
}
return 0
}
if unicode.In(r, unicode.Han, unicode.Hangul, unicode.Katakana) {
return 2
}
return 1
}
// StringWidth returns the number of columns required when printing the string
// to a terminal. The string should not contain the null character, or any
// control character.
func StringWidth(s string) int {
width := 0
for _, r := range s {
width += RuneWidth(r)
}
return width
}