Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions cmd/demo-colors/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# 256-Color Palette Demo

This demo showcases the 256-color ANSI palette support in go-pretty.

## Usage

```bash
go run ./cmd/demo-colors
```

## Screenshot

![256-Color Palette Demo](demo.png)
117 changes: 117 additions & 0 deletions cmd/demo-colors/demo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package main

import (
"fmt"

"github.com/jedib0t/go-pretty/v6/table"
"github.com/jedib0t/go-pretty/v6/text"
)

func main() {
tw := table.NewWriter()
tw.AppendRows([]table.Row{
{renderGrid(false)},
{renderGrid(true)},
})
tw.SetTitle("256-Color Palette")
tw.SetStyle(table.StyleLight)
tw.Style().Options.SeparateRows = true
tw.Style().Title.Align = text.AlignCenter
fmt.Println(tw.Render())
}

func alignCode(code int) string {
return " " + text.AlignRight.Apply(fmt.Sprint(code), 3) + " "
}

func blankTable() table.Writer {
tw := table.NewWriter()
tw.SetStyle(table.StyleLight)
style := tw.Style()
style.Box.PaddingLeft = ""
style.Box.PaddingRight = ""
style.Options.DrawBorder = false
style.Options.SeparateRows = false
style.Options.SeparateColumns = false
return tw
}

func buildRow(start, end int, isBackground bool) table.Row {
row := make(table.Row, 0, end-start)
for i := start; i < end; i++ {
row = append(row, cellValue(i, isBackground))
}
return row
}

func cellValue(code int, isBackground bool) string {
if isBackground {
return text.Colors{text.Bg256Color(code), text.FgBlack}.Sprint(alignCode(code))
}
return text.Colors{text.BgBlack, text.Fg256Color(code)}.Sprint(alignCode(code))
}

func renderGrid(isBackground bool) string {
tw := table.NewWriter()
tw.SetIndexColumn(1)
title := "Foreground Colors"
if isBackground {
title = "Background Colors"
}
tw.SetTitle(text.Underline.Sprint(title) + "\n")
tw.SetStyle(table.StyleLight)
style := tw.Style()
style.Box.PaddingLeft = ""
style.Box.PaddingRight = ""
style.Options.DrawBorder = false
style.Options.SeparateRows = false
style.Options.SeparateColumns = false
style.Title.Align = text.AlignCenter
tw.SetColumnConfigs([]table.ColumnConfig{
{Number: 1, Align: text.AlignCenter},
})

// Standard 16 colors (0-15)
row16 := blankTable()
row16.AppendRow(buildRow(0, 16, isBackground))
tw.AppendRows([]table.Row{{row16.Render()}, {""}})

// RGB cube colors (16-231) - 216 colors in 6 blocks of 36
row216 := blankTable()
blockRow := make(table.Row, 0)
for block := 0; block < 6; block++ {
blockStart := 16 + 36*block
blockTable := blankTable()
colors := buildRow(blockStart, blockStart+36, isBackground)
for i := 0; i < len(colors); i += 6 {
end := i + 6
if end > len(colors) {
end = len(colors)
}
blockTable.AppendRow(colors[i:end])
}
blockRow = append(blockRow, blockTable.Render())
if len(blockRow) == 3 {
row216.AppendRow(blockRow)
blockRow = make(table.Row, 0)
}
}
if len(blockRow) > 0 {
row216.AppendRow(blockRow)
}
tw.AppendRows([]table.Row{{row216.Render()}, {""}})

// Grayscale colors (232-255) - 24 colors
rowGrayscale := blankTable()
colors := buildRow(232, 256, isBackground)
for i := 0; i < len(colors); i += 12 {
end := i + 12
if end > len(colors) {
end = len(colors)
}
rowGrayscale.AppendRow(colors[i:end])
}
tw.AppendRows([]table.Row{{rowGrayscale.Render()}})

return tw.Render()
}
Binary file added cmd/demo-colors/demo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions text/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ Used heavily in the other packages in this repo ([list](../list),
- Foreground colors (Black, Red, Green, Yellow, Blue, Magenta, Cyan, White)
- Background colors (matching foreground set)
- Hi-intensity variants for both foreground and background
- **256-color palette support** - Extended color support for terminals
- Standard 16 colors (0-15)
- RGB cube colors (16-231) - 216 colors organized in a 6x6x6 cube
- Grayscale colors (232-255) - 24 shades of gray
- Helper functions: `Fg256Color(index)`, `Bg256Color(index)`, `Fg256RGB(r, g, b)`, `Bg256RGB(r, g, b)`
- Text attributes (Bold, Faint, Italic, Underline, Blink, Reverse, Concealed, CrossedOut)
- Automatic color detection based on environment variables (`NO_COLOR`, `FORCE_COLOR`, `TERM`)
- Global enable/disable functions for colors
Expand Down Expand Up @@ -76,6 +81,7 @@ Used heavily in the other packages in this repo ([list](../list),
- `EscSeqParser` - Parser for advanced escape sequence parsing and tracking
- Supports both CSI (Control Sequence Introducer) and OSI (Operating System Command) formats
- Tracks active formatting codes and can generate consolidated escape sequences
- Full support for 256-color escape sequences (`\x1b[38;5;n`m` and `\x1b[48;5;n`m`)

### Cursor Control

Expand Down
8 changes: 7 additions & 1 deletion text/ansi_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@

package text

import "os"

func areANSICodesSupported() bool {
return true
// On Unix systems, ANSI codes are generally supported unless TERM is "dumb"
// This is a basic check; 256-color sequences are ANSI sequences and will
// be handled by terminals that support them (or ignored by those that don't)
term := os.Getenv("TERM")
return term != "dumb"
}
159 changes: 149 additions & 10 deletions text/color.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"sync"
)

// colorsEnabled is true if colors are enabled and supported by the terminal.
var colorsEnabled = areColorsOnInTheEnv() && areANSICodesSupported()

// DisableColors (forcefully) disables color coding globally.
Expand All @@ -21,17 +22,24 @@ func EnableColors() {
colorsEnabled = true
}

// areColorsOnInTheEnv returns true is colors are not disable using
// areColorsOnInTheEnv returns true if colors are not disabled using
// well known environment variables.
func areColorsOnInTheEnv() bool {
if os.Getenv("FORCE_COLOR") == "1" {
// FORCE_COLOR takes precedence - if set to a truthy value, enable colors
forceColor := os.Getenv("FORCE_COLOR")
if forceColor != "" && forceColor != "0" && forceColor != "false" {
return true
}
if os.Getenv("NO_COLOR") == "" || os.Getenv("NO_COLOR") == "0" {
return os.Getenv("TERM") != "dumb"

// NO_COLOR: if set to any non-empty value (except "0"), disable colors
// Note: "0" is treated as "not set" to allow explicit enabling via NO_COLOR=0
noColor := os.Getenv("NO_COLOR")
if noColor != "" && noColor != "0" {
return false
}

return false
// Default: check TERM - if not "dumb", assume colors are supported
return os.Getenv("TERM") != "dumb"
}

// The logic here is inspired from github.com/fatih/color; the following is
Expand Down Expand Up @@ -107,8 +115,33 @@ const (
BgHiWhite
)

// 256-color support
// Internal encoding for 256-color codes (used by escape_seq_parser.go):
// Foreground 256-color: Fg256Start + colorIndex (1000-1255)
// Background 256-color: Bg256Start + colorIndex (2000-2255)
const (
// Fg256Start is the base value for 256-color foreground colors.
// Use Fg256Color(index) to create a 256-color foreground color.
Fg256Start Color = 1000
// Bg256Start is the base value for 256-color background colors.
// Use Bg256Color(index) to create a 256-color background color.
Bg256Start Color = 2000
)

// CSSClasses returns the CSS class names for the color.
func (c Color) CSSClasses() string {
// Check for 256-color and convert to RGB-based class
if c >= Fg256Start && c < Fg256Start+256 {
colorIndex := int(c - Fg256Start)
r, g, b := color256ToRGB(colorIndex)
return fmt.Sprintf("fg-256-%d-%d-%d", r, g, b)
}
if c >= Bg256Start && c < Bg256Start+256 {
colorIndex := int(c - Bg256Start)
r, g, b := color256ToRGB(colorIndex)
return fmt.Sprintf("bg-256-%d-%d-%d", r, g, b)
}
// Existing behavior for standard colors
if class, ok := colorCSSClassMap[c]; ok {
return class
}
Expand All @@ -117,6 +150,17 @@ func (c Color) CSSClasses() string {

// EscapeSeq returns the ANSI escape sequence for the color.
func (c Color) EscapeSeq() string {
// Check if it's a 256-color foreground (1000-1255)
if c >= Fg256Start && c < Fg256Start+256 {
colorIndex := int(c - Fg256Start)
return fmt.Sprintf("%s38;5;%d%s", EscapeStart, colorIndex, EscapeStop)
}
// Check if it's a 256-color background (2000-2255)
if c >= Bg256Start && c < Bg256Start+256 {
colorIndex := int(c - Bg256Start)
return fmt.Sprintf("%s48;5;%d%s", EscapeStart, colorIndex, EscapeStop)
}
// Regular color (existing behavior)
return EscapeStart + strconv.Itoa(int(c)) + EscapeStop
}

Expand Down Expand Up @@ -154,7 +198,8 @@ func (c Colors) CSSClasses() string {

var classes []string
for _, color := range c {
if class, ok := colorCSSClassMap[color]; ok {
class := color.CSSClasses()
if class != "" {
classes = append(classes, class)
}
}
Expand All @@ -173,16 +218,32 @@ func (c Colors) EscapeSeq() string {
colorsKey := fmt.Sprintf("%#v", c)
escapeSeq, ok := colorsSeqMap.Load(colorsKey)
if !ok || escapeSeq == "" {
colorNums := make([]string, len(c))
for idx, color := range c {
colorNums[idx] = strconv.Itoa(int(color))
codes := make([]string, 0, len(c))
for _, color := range c {
codes = append(codes, c.colorToCode(color))
}
escapeSeq = EscapeStart + strings.Join(colorNums, ";") + EscapeStop
escapeSeq = EscapeStart + strings.Join(codes, ";") + EscapeStop
colorsSeqMap.Store(colorsKey, escapeSeq)
}
return escapeSeq.(string)
}

// colorToCode converts a Color to its escape sequence code string.
func (c Colors) colorToCode(color Color) string {
// Check if it's a 256-color foreground (1000-1255)
if color >= Fg256Start && color < Fg256Start+256 {
colorIndex := int(color - Fg256Start)
return fmt.Sprintf("38;5;%d", colorIndex)
}
// Check if it's a 256-color background (2000-2255)
if color >= Bg256Start && color < Bg256Start+256 {
colorIndex := int(color - Bg256Start)
return fmt.Sprintf("48;5;%d", colorIndex)
}
// Regular color
return strconv.Itoa(int(color))
}

// HTMLProperty returns the "class" attribute for the colors.
func (c Colors) HTMLProperty() string {
classes := c.CSSClasses()
Expand All @@ -208,3 +269,81 @@ func colorize(s string, escapeSeq string) string {
}
return Escape(s, escapeSeq)
}

// Fg256Color returns a foreground 256-color Color value.
// The index must be in the range 0-255.
func Fg256Color(index int) Color {
if index < 0 || index > 255 {
return Reset
}
return Fg256Start + Color(index)
}

// Bg256Color returns a background 256-color Color value.
// The index must be in the range 0-255.
func Bg256Color(index int) Color {
if index < 0 || index > 255 {
return Reset
}
return Bg256Start + Color(index)
}

// Fg256RGB returns a foreground 256-color from RGB values in the 6x6x6 color cube.
// Each RGB component must be in the range 0-5.
// The resulting color index will be in the range 16-231.
func Fg256RGB(r, g, b int) Color {
if r < 0 || r > 5 || g < 0 || g > 5 || b < 0 || b > 5 {
return Reset
}
index := 16 + (r*36 + g*6 + b)
return Fg256Color(index)
}

// Bg256RGB returns a background 256-color from RGB values in the 6x6x6 color cube.
// Each RGB component must be in the range 0-5.
// The resulting color index will be in the range 16-231.
func Bg256RGB(r, g, b int) Color {
if r < 0 || r > 5 || g < 0 || g > 5 || b < 0 || b > 5 {
return Reset
}
index := 16 + (r*36 + g*6 + b)
return Bg256Color(index)
}

// color256ToRGB converts a 256-color index to RGB values.
// Returns (r, g, b) values in the range 0-255.
func color256ToRGB(index int) (r, g, b int) {
if index < 16 {
// Standard 16 colors - map to predefined RGB values
standardColors := [16][3]int{
{0, 0, 0}, // 0: black
{128, 0, 0}, // 1: red
{0, 128, 0}, // 2: green
{128, 128, 0}, // 3: yellow
{0, 0, 128}, // 4: blue
{128, 0, 128}, // 5: magenta
{0, 128, 128}, // 6: cyan
{192, 192, 192}, // 7: light gray
{128, 128, 128}, // 8: dark gray
{255, 0, 0}, // 9: bright red
{0, 255, 0}, // 10: bright green
{255, 255, 0}, // 11: bright yellow
{0, 0, 255}, // 12: bright blue
{255, 0, 255}, // 13: bright magenta
{0, 255, 255}, // 14: bright cyan
{255, 255, 255}, // 15: white
}
return standardColors[index][0], standardColors[index][1], standardColors[index][2]
} else if index < 232 {
// 216-color RGB cube (16-231)
index -= 16
r = (index / 36) * 51
g = ((index / 6) % 6) * 51
b = (index % 6) * 51
} else {
// 24 grayscale colors (232-255)
gray := 8 + (index-232)*10
r, g, b = gray, gray, gray
}
return
}
Loading