diff --git a/colorpicker/picker.go b/colorpicker/picker.go new file mode 100644 index 0000000..8583981 --- /dev/null +++ b/colorpicker/picker.go @@ -0,0 +1,285 @@ +package colorpicker + +import ( + "fmt" + "image/color" + "strings" + + tea "github.com/charmbracelet/bubbletea/v2" + "github.com/charmbracelet/lipgloss" + colorconv "github.com/charmbracelet/x/exp/color" +) + +type mode int + +const ( + modeRGB mode = iota + modeHSV +) + +type Styles struct { + Base lipgloss.Style + Frame lipgloss.Style + FocusedValue Value + BlurredValue Value +} + +type Value struct { + Label lipgloss.Style + Value lipgloss.Style + Unit lipgloss.Style + Decr lipgloss.Style + Incr lipgloss.Style +} + +func DefaultStyles() (s Styles) { + s.Base = lipgloss.NewStyle(). + Background(lipgloss.Color("#191919")). + Foreground(lipgloss.Color("#F2F2F2")) + s.Frame = s.Base. + Margin(2, 4). + Padding(2, 4) + + var v Value + v.Label = s.Base. + Foreground(lipgloss.Color("241")) + v.Value = s.Base. + Foreground(lipgloss.Color("241")) + v.Unit = s.Base. + Foreground(lipgloss.Color("241")) + v.Decr = s.Base. + Foreground(lipgloss.Color("205")). + SetString("◀ ") + v.Incr = s.Base. + Foreground(lipgloss.Color("82")). + SetString(" ▶") + + s.FocusedValue = v + s.BlurredValue = v + + return s +} + +type Model struct { + Mode mode + Styles Styles + color colorconv.Color + labels []string + inputs []int + index int +} + +func (m Model) Init() (tea.Model, tea.Cmd) { + return m.InitAs() +} + +func (m Model) InitAs() (Model, tea.Cmd) { + m.Styles = DefaultStyles() + m.inputs = make([]int, 3) + m.SetColor(color.RGBA{0, 0, 0, 255}) + return m, nil +} + +func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + return m.UpdateAs(msg) +} + +func (m Model) UpdateAs(msg tea.Msg) (Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyPressMsg: + switch { + case msg.Code == tea.KeyTab: + m.index++ + if m.index >= len(m.inputs) { + m.index = 0 + } + return m, nil + + case msg.Code == tea.KeySpace: + m.updateColorFromInputs() + switch m.Mode { + case modeRGB: + r, g, b, _ := m.color.RGBA() + m.inputs[0] = int(r) + m.inputs[1] = int(g) + m.inputs[2] = int(b) + m.Mode = modeHSV + case modeHSV: + h, s, v := m.color.HSV() + m.inputs[0] = int(h) + m.inputs[1] = int(s * 100) + m.inputs[2] = int(v * 100) + m.Mode = modeRGB + } + return m, nil + + case msg.Code == tea.KeyTab && msg.Mod == tea.ModShift: + m.inputs[m.index]++ + return m, nil + + case msg.Code == tea.KeyLeft || msg.Code == 'h': + decr := 1 + if msg.Mod == tea.ModShift { + decr = 10 + } + m.inputs[m.index] -= decr + + switch m.Mode { + case modeRGB: + if m.inputs[m.index] < 0 { + m.inputs[m.index] = 255 + } + case modeHSV: + switch m.index { + case 0: + if m.inputs[m.index] < 0 { + m.inputs[m.index] = 360 + } + case 1, 2: + if m.inputs[m.index] < 0 { + m.inputs[m.index] = 100 + } + } + } + + m.updateColorFromInputs() + return m, nil + + case msg.Code == tea.KeyRight || msg.Code == 'l': + incr := 1 + if msg.Mod == tea.ModShift { + incr = 10 + } + m.inputs[m.index] += incr + + switch m.Mode { + case modeRGB: + if m.inputs[m.index] > 255 { + m.inputs[m.index] = 0 + } + case modeHSV: + switch m.index { + case 0: + if m.inputs[m.index] > 360 { + m.inputs[m.index] = 0 + } + case 1, 2: + if m.inputs[m.index] > 100 { + m.inputs[m.index] = 0 + } + } + } + + m.updateColorFromInputs() + return m, nil + + case msg.Code == tea.KeyEnter: + return m, tea.SetClipboard(m.Hex()) + } + } + + return m, nil +} + +func (m *Model) updateColorFromInputs() { + switch m.Mode { + case modeRGB: + m.color.FromRGB( + uint8(m.inputs[0]), + uint8(m.inputs[1]), + uint8(m.inputs[2]), + ) + case modeHSV: + m.color.FromHSV( + float64(m.inputs[0]), + float64(m.inputs[1])/100, + float64(m.inputs[2])/100, + ) + } +} + +func (i *Model) SetColor(c color.Color) { + r, g, b, _ := c.RGBA() + i.color.FromRGB(uint8(r), uint8(g), uint8(b)) + switch i.Mode { + case modeRGB: + r, g, b, _ := c.RGBA() + i.inputs[0] = int(r) + i.inputs[1] = int(g) + i.inputs[2] = int(b) + case modeHSV: + h, s, v := i.color.HSV() + i.inputs[0] = int(h) + i.inputs[1] = int(s) + i.inputs[2] = int(v) + } + return +} + +func (m Model) View() string { + labels := make([]string, len(m.inputs)) + switch m.Mode { + case modeRGB: + labels = []string{"R", "G", "B"} + case modeHSV: + labels = []string{"H", "S", "V"} + } + + // ansi256 := colorprofile.ANSI256.Convert(i.color.Color) + // ansi := colorprofile.ANSI.Convert(i.color.Color) + + var ( + b strings.Builder + s = m.Styles + p = func(args ...any) { + fmt.Fprint(&b, args...) + } + ) + + const block = " " + + c := lipgloss.Color(m.Hex()) + fg := s.Base.Foreground(c) + bg := s.Base.Background(c) + p(bg.Render(block)) + p(fg.Render(" " + m.Hex())) + + // b.WriteString(lipgloss.NewStyle().Background(lipgloss.Color(ansi256)).Render(block)) + // b.WriteString(" ") + // b.WriteString(lipgloss.NewStyle().Background(lipgloss.Color(ansi)).Render(block)) + + b.WriteString("\n\n") + + for j, in := range m.inputs { + var v Value + if m.index == j { + v = s.FocusedValue + p(s.FocusedValue.Decr.String()) + } else { + v = s.BlurredValue + p(s.Base.Render(" ")) + } + p(v.Label.Render(labels[j] + " ")) + p(v.Value.Render(fmt.Sprintf("%3d", in))) + if m.Mode == modeHSV { + switch j { + case 0: + p(v.Unit.Render("°")) + case 1, 2: + p(v.Unit.Render("%")) + } + } else { + p(v.Unit.Render(" ")) + } + if m.index == j { + p(v.Incr.String()) + } else { + p(s.Base.Render(" ")) + } + } + return s.Frame.Render(b.String()) +} + +func (i Model) Hex() string { + return i.color.Hex() +} diff --git a/go.mod b/go.mod index d44bd12..83b4e32 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/charmbracelet/harmonica v0.2.0 github.com/charmbracelet/lipgloss v0.13.0 github.com/charmbracelet/x/ansi v0.3.2 + github.com/charmbracelet/x/exp/color v0.0.0-20240921120225-1488dc3dde36 github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b github.com/dustin/go-humanize v1.0.1 github.com/lucasb-eyer/go-colorful v1.2.0 diff --git a/go.sum b/go.sum index 154e4dc..7d2bf60 100644 --- a/go.sum +++ b/go.sum @@ -14,6 +14,8 @@ github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY= github.com/charmbracelet/x/ansi v0.3.2 h1:wsEwgAN+C9U06l9dCVMX0/L3x7ptvY1qmjMwyfE6USY= github.com/charmbracelet/x/ansi v0.3.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/x/exp/color v0.0.0-20240921120225-1488dc3dde36 h1:+oVvdnVuR2+4QHnjbgV/G19Bv11Mz5K47ztmUHWpUcI= +github.com/charmbracelet/x/exp/color v0.0.0-20240921120225-1488dc3dde36/go.mod h1:plRnjm55MmwZ2yCD3QaHsjUhbFmfwJaP3pmPkM/BNcM= github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b h1:MnAMdlwSltxJyULnrYbkZpp4k58Co7Tah3ciKhSNo0Q= github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0=