Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: query and set terminal background, foreground, and cursor colors #1085

Merged
merged 5 commits into from
Aug 19, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
136 changes: 136 additions & 0 deletions color.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,65 @@
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 @@
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 @@
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 @@
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 @@
}
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)

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
caarlos0 marked this conversation as resolved.
Show resolved Hide resolved
}

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)

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)

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)

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)

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)

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)

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)

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)

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)

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)

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
Loading