Skip to content

Commit

Permalink
feat: query and set terminal background, foreground, and cursor colors (
Browse files Browse the repository at this point in the history
#1085)

This adds the ability to read and set terminal background, foreground,
and cursor color.

Supersedes: #567
Fixes: #207
  • Loading branch information
aymanbagabas authored Aug 19, 2024
2 parents c657bba + 19f916b commit 485e52e
Show file tree
Hide file tree
Showing 4 changed files with 380 additions and 0 deletions.
136 changes: 136 additions & 0 deletions color.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,65 @@ package tea
import (
"fmt"
"image/color"
"math"
"strconv"
"strings"
)

// backgroundColorMsg is a message that requests the terminal background color.
type backgroundColorMsg struct{}

// BackgroundColor is a command that requests the terminal background color.
func BackgroundColor() Msg {
return backgroundColorMsg{}
}

// foregroundColorMsg is a message that requests the terminal foreground color.
type foregroundColorMsg struct{}

// ForegroundColor is a command that requests the terminal foreground color.
func ForegroundColor() Msg {
return foregroundColorMsg{}
}

// cursorColorMsg is a message that requests the terminal cursor color.
type cursorColorMsg struct{}

// CursorColor is a command that requests the terminal cursor color.
func CursorColor() Msg {
return cursorColorMsg{}
}

// setBackgroundColorMsg is a message that sets the terminal background color.
type setBackgroundColorMsg struct{ color.Color }

// SetBackgroundColor is a command that sets the terminal background color.
func SetBackgroundColor(c color.Color) Cmd {
return func() Msg {
return setBackgroundColorMsg{c}
}
}

// setForegroundColorMsg is a message that sets the terminal foreground color.
type setForegroundColorMsg struct{ color.Color }

// SetForegroundColor is a command that sets the terminal foreground color.
func SetForegroundColor(c color.Color) Cmd {
return func() Msg {
return setForegroundColorMsg{c}
}
}

// setCursorColorMsg is a message that sets the terminal cursor color.
type setCursorColorMsg struct{ color.Color }

// SetCursorColor is a command that sets the terminal cursor color.
func SetCursorColor(c color.Color) Cmd {
return func() Msg {
return setCursorColorMsg{c}
}
}

// ForegroundColorMsg represents a foreground color message.
// This message is emitted when the program requests the terminal foreground
// color.
Expand All @@ -17,6 +72,11 @@ func (e ForegroundColorMsg) String() string {
return colorToHex(e)
}

// IsDark returns whether the color is dark.
func (e ForegroundColorMsg) IsDark() bool {
return isDarkColor(e)
}

// BackgroundColorMsg represents a background color message.
// This message is emitted when the program requests the terminal background
// color.
Expand All @@ -27,6 +87,11 @@ func (e BackgroundColorMsg) String() string {
return colorToHex(e)
}

// IsDark returns whether the color is dark.
func (e BackgroundColorMsg) IsDark() bool {
return isDarkColor(e)
}

// CursorColorMsg represents a cursor color change message.
// This message is emitted when the program requests the terminal cursor color.
type CursorColorMsg struct{ color.Color }
Expand All @@ -36,6 +101,11 @@ func (e CursorColorMsg) String() string {
return colorToHex(e)
}

// IsDark returns whether the color is dark.
func (e CursorColorMsg) IsDark() bool {
return isDarkColor(e)
}

type shiftable interface {
~uint | ~uint16 | ~uint32 | ~uint64
}
Expand Down Expand Up @@ -80,3 +150,69 @@ func xParseColor(s string) color.Color {
}
return color.Black
}

func getMaxMin(a, b, c float64) (max, min float64) {

Check failure on line 154 in color.go

View workflow job for this annotation

GitHub Actions / lint

named return max has same name as predeclared identifier (predeclared)
// TODO: use go1.21 min/max functions
if a > b {
max = a
min = b
} else {
max = b
min = a
}
if c > max {
max = c
} else if c < min {
min = c
}
return max, min
}

func round(x float64) float64 {
return math.Round(x*1000) / 1000

Check failure on line 172 in color.go

View workflow job for this annotation

GitHub Actions / lint-soft

Magic number: 1000, in <argument> detected (gomnd)
}

// rgbToHSL converts an RGB triple to an HSL triple.
func rgbToHSL(r, g, b uint8) (h, s, l float64) {
// convert uint32 pre-multiplied value to uint8
// The r,g,b values are divided by 255 to change the range from 0..255 to 0..1:
Rnot := float64(r) / 255

Check failure on line 179 in color.go

View workflow job for this annotation

GitHub Actions / lint-soft

Magic number: 255, in <operation> detected (gomnd)
Gnot := float64(g) / 255

Check failure on line 180 in color.go

View workflow job for this annotation

GitHub Actions / lint-soft

Magic number: 255, in <operation> detected (gomnd)
Bnot := float64(b) / 255

Check failure on line 181 in color.go

View workflow job for this annotation

GitHub Actions / lint-soft

Magic number: 255, in <operation> detected (gomnd)
Cmax, Cmin := getMaxMin(Rnot, Gnot, Bnot)
Δ := Cmax - Cmin
// Lightness calculation:
l = (Cmax + Cmin) / 2

Check failure on line 185 in color.go

View workflow job for this annotation

GitHub Actions / lint-soft

Magic number: 2, in <operation> detected (gomnd)
// Hue and Saturation Calculation:
if Δ == 0 {
h = 0
s = 0
} else {
switch Cmax {
case Rnot:
h = 60 * (math.Mod((Gnot-Bnot)/Δ, 6))

Check failure on line 193 in color.go

View workflow job for this annotation

GitHub Actions / lint-soft

Magic number: 6, in <argument> detected (gomnd)
case Gnot:
h = 60 * (((Bnot - Rnot) / Δ) + 2)

Check failure on line 195 in color.go

View workflow job for this annotation

GitHub Actions / lint-soft

Magic number: 60, in <operation> detected (gomnd)
case Bnot:
h = 60 * (((Rnot - Gnot) / Δ) + 4)

Check failure on line 197 in color.go

View workflow job for this annotation

GitHub Actions / lint-soft

Magic number: 60, in <operation> detected (gomnd)
}
if h < 0 {
h += 360
}

s = Δ / (1 - math.Abs((2*l)-1))

Check failure on line 203 in color.go

View workflow job for this annotation

GitHub Actions / lint-soft

Magic number: 2, in <operation> detected (gomnd)
}

return h, round(s), round(l)
}

// isDarkColor returns whether the given color is dark.
func isDarkColor(c color.Color) bool {
if c == nil {
return true
}

r, g, b, _ := c.RGBA()
_, _, l := rgbToHSL(uint8(r>>8), uint8(g>>8), uint8(b>>8))

Check failure on line 216 in color.go

View workflow job for this annotation

GitHub Actions / lint-soft

Magic number: 8, in <argument> detected (gomnd)
return l < 0.5
}
209 changes: 209 additions & 0 deletions examples/set-terminal-color/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
package main

import (
"image/color"
"log"
"strings"

"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/lucasb-eyer/go-colorful"
)

type colorType int

const (
foreground colorType = iota + 1
background
cursor
)

func (c colorType) String() string {
switch c {
case foreground:
return "Foreground"
case background:
return "Background"
case cursor:
return "Cursor"
default:
return "Unknown"
}
}

type state int

const (
chooseState state = iota
inputState
)

type model struct {
ti textinput.Model
choice colorType
state state
choiceIndex int
err error
bg color.Color
fg color.Color
cursor color.Color
}

func (m model) Init() tea.Cmd {
return tea.Batch(
textinput.Blink,
tea.BackgroundColor,
tea.ForegroundColor,
tea.CursorColor,
)
}

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c", "q":
var cmds []tea.Cmd
if m.fg != nil {
cmds = append(cmds, tea.SetForegroundColor(m.fg))
}
if m.bg != nil {
cmds = append(cmds, tea.SetBackgroundColor(m.bg))
}
if m.cursor != nil {
cmds = append(cmds, tea.SetCursorColor(m.cursor))
}
cmds = append(cmds, tea.Quit)
return m, tea.Batch(cmds...)
}

switch m.state {
case chooseState:
switch msg.String() {
case "j", "down":
m.choiceIndex++
if m.choiceIndex > 2 {
m.choiceIndex = 0
}
case "k", "up":
m.choiceIndex--
if m.choiceIndex < 0 {
m.choiceIndex = 2
}
case "enter":
m.state = inputState
switch m.choiceIndex {
case 0:
m.choice = foreground
case 1:
m.choice = background
case 2:
m.choice = cursor
}
}

case inputState:
switch msg.String() {
case "esc":
m.choice = 0
m.choiceIndex = 0
m.state = chooseState
m.err = nil
case "enter":
val := m.ti.Value()
col, err := colorful.Hex(val)
if err != nil {
m.err = err
} else {
m.err = nil
choice := m.choice
m.choice = 0
m.choiceIndex = 0
m.state = chooseState

switch choice {
case foreground:
return m, tea.SetForegroundColor(col)
case background:
return m, tea.SetBackgroundColor(col)
case cursor:
return m, tea.SetCursorColor(col)
}
}

default:
var cmd tea.Cmd
m.ti, cmd = m.ti.Update(msg)
return m, cmd
}
}

case tea.BackgroundColorMsg:
m.bg = msg.Color

case tea.ForegroundColorMsg:
m.fg = msg.Color

case tea.CursorColorMsg:
m.cursor = msg.Color

}

return m, nil
}

func (m model) View() string {
var s strings.Builder

switch m.state {
case chooseState:
s.WriteString("Choose a color to set:\n\n")
for i, c := range []colorType{foreground, background, cursor} {
if i == m.choiceIndex {
s.WriteString(" > ")
} else {
s.WriteString(" ")
}
s.WriteString(c.String())
s.WriteString("\n")
}
case inputState:
s.WriteString("Enter a color in hex format:\n\n")
s.WriteString(m.ti.View())
s.WriteString("\n")
}

if m.err != nil {
s.WriteString("\nError: ")
s.WriteString(m.err.Error())
}

s.WriteString("\nPress q to quit")

switch m.state {
case chooseState:
s.WriteString(", j/k to move, and enter to select")
case inputState:
s.WriteString(", and enter to submit, esc to go back")
}

s.WriteString("\n")

return s.String()
}

func main() {
ti := textinput.New()
ti.Placeholder = "#ff00ff"
ti.Focus()
ti.CharLimit = 156
ti.Width = 20
p := tea.NewProgram(model{
ti: ti,
})

_, err := p.Run()
if err != nil {
log.Fatalf("Error running program: %v", err)
}
}
Loading

0 comments on commit 485e52e

Please sign in to comment.