summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorBenjamin Chausse <benjamin@chausse.xyz>2024-11-23 18:12:03 -0500
committerBenjamin Chausse <benjamin@chausse.xyz>2024-11-23 18:12:03 -0500
commit89094fecf4cb1c018f15c976641cd18c255eac28 (patch)
tree9f6e32c38013bc526399ab324891e0b3269e50dc /internal
Semi-working POC
Diffstat (limited to 'internal')
-rw-r--r--internal/colors/cmyk.go46
-rw-r--r--internal/colors/hsl.go1
-rw-r--r--internal/colors/precise.go38
-rw-r--r--internal/colors/rgb.go25
-rw-r--r--internal/picker/keys.go29
-rw-r--r--internal/picker/picker.go137
-rw-r--r--internal/quit/quit.go17
-rw-r--r--internal/slider/evaluation.go46
-rw-r--r--internal/slider/keys.go52
-rw-r--r--internal/slider/slider.go71
-rw-r--r--internal/switcher/keys.go33
-rw-r--r--internal/switcher/switcher.go89
-rw-r--r--internal/util/util.go12
13 files changed, 596 insertions, 0 deletions
diff --git a/internal/colors/cmyk.go b/internal/colors/cmyk.go
new file mode 100644
index 0000000..3460c4e
--- /dev/null
+++ b/internal/colors/cmyk.go
@@ -0,0 +1,46 @@
+package colors
+
+import "math"
+
+type CMYK struct {
+ C int // 0-100
+ M int // 0-100
+ Y int // 0-100
+ K int // 0-100
+}
+
+func (c CMYK) ToPrecise() PreciseColor {
+ return PreciseColor{
+ R: (1 - float64(c.C)/100) * (1 - float64(c.K)/100),
+ G: (1 - float64(c.M)/100) * (1 - float64(c.K)/100),
+ B: (1 - float64(c.Y)/100) * (1 - float64(c.K)/100),
+ }
+}
+
+func (c CMYK) FromPrecise(p PreciseColor) ColorSpace {
+ // Extract RGB components from the PreciseColor
+ r := p.R
+ g := p.G
+ b := p.B
+
+ // Calculate the K (key/black) component
+ k := 1 - math.Max(math.Max(r, g), b)
+
+ // Avoid division by zero when K is 1 (pure black)
+ if k == 1 {
+ return CMYK{C: 0, M: 0, Y: 0, K: 100}
+ }
+
+ // Calculate the CMY components based on the remaining color values
+ cyan := (1 - r - k) / (1 - k)
+ magenta := (1 - g - k) / (1 - k)
+ yellow := (1 - b - k) / (1 - k)
+
+ // Scale to 0-100 and return
+ return CMYK{
+ C: int(math.Round(cyan * 100)),
+ M: int(math.Round(magenta * 100)),
+ Y: int(math.Round(yellow * 100)),
+ K: int(math.Round(k * 100)),
+ }
+}
diff --git a/internal/colors/hsl.go b/internal/colors/hsl.go
new file mode 100644
index 0000000..7477042
--- /dev/null
+++ b/internal/colors/hsl.go
@@ -0,0 +1 @@
+package colors
diff --git a/internal/colors/precise.go b/internal/colors/precise.go
new file mode 100644
index 0000000..fafa104
--- /dev/null
+++ b/internal/colors/precise.go
@@ -0,0 +1,38 @@
+package colors
+
+import (
+ "fmt"
+ "math"
+ "strings"
+)
+
+type ColorSpace interface {
+ ToPrecise() PreciseColor
+ FromPrecise(PreciseColor) ColorSpace
+}
+
+// PreciseColor is a color with floating point values for red, green, and blue.
+// The extra precision minimizes rounding errors when converting between different
+// color spaces. It is used as an intermediate representation when converting between
+// different color spaces.
+type PreciseColor struct {
+ R, G, B float64
+}
+
+func (c PreciseColor) ToPrecise() PreciseColor {
+ return c
+}
+
+func (c PreciseColor) FromPrecise(p PreciseColor) ColorSpace {
+ return p
+}
+
+func ToHexString(cs ColorSpace) string {
+ p := cs.ToPrecise()
+
+ return strings.ToUpper(fmt.Sprintf("#%02x%02x%02x",
+ int(math.Round(p.R*255)),
+ int(math.Round(p.G*255)),
+ int(math.Round(p.B*255)),
+ ))
+}
diff --git a/internal/colors/rgb.go b/internal/colors/rgb.go
new file mode 100644
index 0000000..e81aefc
--- /dev/null
+++ b/internal/colors/rgb.go
@@ -0,0 +1,25 @@
+package colors
+
+import "math"
+
+type RGB struct {
+ R int // 0-255
+ G int // 0-255
+ B int // 0-255
+}
+
+func (c RGB) ToPrecise() PreciseColor {
+ return PreciseColor{
+ R: float64(c.R) / 255,
+ G: float64(c.G) / 255,
+ B: float64(c.B) / 255,
+ }
+}
+
+func (c RGB) FromPrecise(p PreciseColor) ColorSpace {
+ return RGB{
+ R: int(math.Round(p.R * 255)),
+ G: int(math.Round(p.G * 255)),
+ B: int(math.Round(p.B * 255)),
+ }
+}
diff --git a/internal/picker/keys.go b/internal/picker/keys.go
new file mode 100644
index 0000000..f5b7135
--- /dev/null
+++ b/internal/picker/keys.go
@@ -0,0 +1,29 @@
+package picker
+
+import "github.com/charmbracelet/bubbles/key"
+
+type keybinds struct {
+ next, prev key.Binding
+}
+
+func newKeybinds() keybinds {
+ return keybinds{
+ next: key.NewBinding(
+ key.WithKeys("j", "down"),
+ key.WithHelp("j", "previous slider"),
+ ),
+ prev: key.NewBinding(
+ key.WithKeys("k", "up"),
+ key.WithHelp("k", "next slider"),
+ ),
+ }
+}
+
+func Keys() []key.Binding {
+ k := newKeybinds()
+ return []key.Binding{k.next, k.prev}
+}
+
+func (m Model) AllKeys() []key.Binding {
+ return append(Keys(), m.sliders[m.active].AllKeys()...)
+}
diff --git a/internal/picker/picker.go b/internal/picker/picker.go
new file mode 100644
index 0000000..e3c396e
--- /dev/null
+++ b/internal/picker/picker.go
@@ -0,0 +1,137 @@
+package picker
+
+import (
+ "fmt"
+
+ "github.com/charmbracelet/bubbles/key"
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/bubbletea-app-template/internal/colors"
+ "github.com/charmbracelet/bubbletea-app-template/internal/slider"
+)
+
+type Model struct {
+ title string
+ active int
+ sliders []slider.Model
+}
+
+func (m *Model) Next() int {
+ m.active = m.fixSel(m.active + 1)
+ return m.active
+}
+
+func (m *Model) Prev() int {
+ m.active = m.fixSel(m.active - 1)
+ return m.active
+}
+
+func (m *Model) Sel(i int) int {
+ m.active = m.fixSel(i)
+ return m.active
+}
+
+func (m Model) fixSel(val int) int {
+ size := len(m.sliders)
+ return (val%size + size) % size
+}
+
+func New(sliders []slider.Model, title string) *Model {
+ return &Model{
+ title: title,
+ active: 0,
+ sliders: sliders,
+ }
+}
+
+func (m Model) Title() string {
+ return m.title
+}
+
+func (m Model) GetColor() colors.ColorSpace {
+ switch m.title {
+ case "RGB":
+ return colors.RGB{
+ R: m.sliders[0].Val(),
+ G: m.sliders[1].Val(),
+ B: m.sliders[2].Val(),
+ }
+ case "CMYK":
+ return colors.CMYK{
+ C: m.sliders[0].Val(),
+ M: m.sliders[1].Val(),
+ Y: m.sliders[2].Val(),
+ K: m.sliders[3].Val(),
+ }
+ // TODO: HSL
+ default: // Default to white if we don't know the color space
+ return colors.RGB{
+ R: 255,
+ G: 255,
+ B: 255,
+ }
+ }
+}
+
+func (m Model) SetColor(c colors.ColorSpace) {
+ p := c.ToPrecise()
+ switch m.title {
+ case "RGB":
+ rgb := colors.RGB{}.FromPrecise(p).(colors.RGB)
+ m.sliders[0].Set(rgb.R)
+ m.sliders[1].Set(rgb.G)
+ m.sliders[2].Set(rgb.B)
+ case "CMYK":
+ cmyk := colors.CMYK{}.FromPrecise(p).(colors.CMYK)
+ m.sliders[0].Set(cmyk.C)
+ m.sliders[1].Set(cmyk.M)
+ m.sliders[2].Set(cmyk.Y)
+ m.sliders[3].Set(cmyk.K)
+ }
+}
+
+func (m Model) Init() tea.Cmd {
+ cmds := []tea.Cmd{}
+ for _, s := range m.sliders {
+ cmds = append(cmds, s.Init())
+ }
+ return tea.Batch(cmds...)
+}
+
+func (m Model) View() string {
+ var s string
+ for i, slider := range m.sliders {
+ if i == m.active {
+ s += fmt.Sprintf("\n-> %s", slider.View())
+ } else {
+ s += fmt.Sprintf("\n %s", slider.View())
+ }
+ }
+ return s
+}
+
+func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ keys := newKeybinds()
+ cmds := []tea.Cmd{}
+ switch msg := msg.(type) {
+ case tea.KeyMsg:
+ switch {
+ case key.Matches(msg, keys.next):
+ m.Next()
+ case key.Matches(msg, keys.prev):
+ m.Prev()
+ default:
+ newActive, cmd := m.sliders[m.active].Update(msg)
+ m.sliders[m.active] = newActive.(slider.Model)
+ cmds = append(cmds, cmd)
+ return m, tea.Batch(cmds...)
+ }
+ }
+ // Keys are only sent to the active sliders
+ // However, other messages (ex: tick, resize) must be sent to all
+ for i, s := range m.sliders {
+ newSlider, cmd := s.Update(msg)
+ m.sliders[i] = newSlider.(slider.Model)
+ cmds = append(cmds, cmd)
+ }
+ return m, tea.Batch(cmds...)
+}
diff --git a/internal/quit/quit.go b/internal/quit/quit.go
new file mode 100644
index 0000000..de1e3b4
--- /dev/null
+++ b/internal/quit/quit.go
@@ -0,0 +1,17 @@
+package quit
+
+import tea "github.com/charmbracelet/bubbletea"
+
+type Model struct{}
+
+func (m Model) Init() tea.Cmd {
+ return nil
+}
+
+func (m Model) Update(tea.Msg) (tea.Model, tea.Cmd) {
+ return m, nil
+}
+
+func (m Model) View() string {
+ return "Goodbye!\n"
+}
diff --git a/internal/slider/evaluation.go b/internal/slider/evaluation.go
new file mode 100644
index 0000000..363efe7
--- /dev/null
+++ b/internal/slider/evaluation.go
@@ -0,0 +1,46 @@
+package slider
+
+func (m Model) Val() int { return m.current }
+
+func (m *Model) Set(v int) {
+ m.current = v
+ m.fixRange()
+}
+
+func (m *Model) Inc(v int) {
+ m.current += v
+ m.fixRange()
+}
+
+func (m *Model) Dec(v int) {
+ m.current -= v
+ m.fixRange()
+}
+
+func (m *Model) Pcnt() float64 {
+ return float64(m.current) / float64(m.max)
+}
+
+func (m *Model) SetPcnt(p float64) {
+ m.current = int(float64(m.max) * p)
+ m.fixRange()
+}
+
+func (m *Model) IncPcnt(p float64) {
+ m.current += int(float64(m.max) * p)
+ m.fixRange()
+}
+
+func (m *Model) DecPcnt(p float64) {
+ m.current -= int(float64(m.max) * p)
+ m.fixRange()
+}
+
+func (m *Model) fixRange() {
+ if m.current > m.max {
+ m.current = m.max
+ }
+ if m.current < 0 {
+ m.current = 0
+ }
+}
diff --git a/internal/slider/keys.go b/internal/slider/keys.go
new file mode 100644
index 0000000..9f96903
--- /dev/null
+++ b/internal/slider/keys.go
@@ -0,0 +1,52 @@
+package slider
+
+import "github.com/charmbracelet/bubbles/key"
+
+type keybinds struct {
+ incRegular key.Binding
+ decRegular key.Binding
+ incPrecise key.Binding
+ decPrecise key.Binding
+ // quitApp key.Binding
+}
+
+func newKeybinds() keybinds {
+ return keybinds{
+ incRegular: key.NewBinding(
+ key.WithKeys("right", "l"),
+ key.WithHelp("l", "Increase (coarse)"),
+ ),
+ decRegular: key.NewBinding(
+ key.WithKeys("left", "h"),
+ key.WithHelp("h", "Decrease (coarse)"),
+ ),
+ incPrecise: key.NewBinding(
+ key.WithKeys("shift+right", "L"),
+ key.WithHelp("L", "Increase (fine)"),
+ ),
+ decPrecise: key.NewBinding(
+ key.WithKeys("shift+left", "H"),
+ key.WithHelp("H", "Decrease (fine)"),
+ ),
+ }
+}
+
+// Join all keybindings into a single slice
+// a parent can use to know what Keys
+// it's children have.
+func Keys() []key.Binding {
+ k := newKeybinds()
+ return []key.Binding{
+ k.incRegular,
+ k.decRegular,
+ k.incPrecise,
+ k.decPrecise,
+ }
+}
+
+// AllKeys returns key.Bindings for the Model
+// and all of its active children. The parent
+// can use this to generate help text.
+func (m Model) AllKeys() []key.Binding {
+ return Keys()
+}
diff --git a/internal/slider/slider.go b/internal/slider/slider.go
new file mode 100644
index 0000000..d726e7b
--- /dev/null
+++ b/internal/slider/slider.go
@@ -0,0 +1,71 @@
+package slider
+
+import (
+ "fmt"
+
+ "github.com/charmbracelet/bubbles/key"
+ "github.com/charmbracelet/bubbles/progress"
+ tea "github.com/charmbracelet/bubbletea"
+)
+
+type Model struct {
+ label byte
+ progress progress.Model
+ max int
+ current int
+ mappings keybinds
+}
+
+func New(label byte, maxVal int, opts ...progress.Option) Model {
+ slider := Model{
+ label: label,
+ progress: progress.New(
+ progress.WithoutPercentage(),
+ ),
+ max: maxVal,
+ current: maxVal / 2,
+ mappings: newKeybinds(),
+ }
+ for _, opt := range opts {
+ opt(&slider.progress)
+ }
+ return slider
+}
+
+func (m Model) Title() string { return fmt.Sprintf("%c", m.label) }
+
+func (m Model) Init() tea.Cmd {
+ return nil
+}
+
+func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ keys := newKeybinds()
+ switch msg := msg.(type) {
+ case tea.KeyMsg:
+ switch {
+ case key.Matches(msg, keys.incRegular):
+ m.IncPcnt(0.05)
+ case key.Matches(msg, keys.decRegular):
+ m.DecPcnt(0.05)
+ case key.Matches(msg, keys.incPrecise):
+ m.Inc(1)
+ case key.Matches(msg, keys.decPrecise):
+ m.Dec(1)
+ }
+ return m, m.progress.SetPercent(m.Pcnt())
+ case progress.FrameMsg:
+ progressModel, cmd := m.progress.Update(msg)
+ m.progress = progressModel.(progress.Model)
+ return m, cmd
+ default:
+ return m, nil
+ }
+}
+
+func (m Model) ViewValue(current int) string {
+ return fmt.Sprintf("(%3d/%d)", current, m.max)
+}
+
+func (m Model) View() string {
+ return fmt.Sprintf("%v: %v %v", m.Title(), m.progress.View(), m.ViewValue(m.current))
+}
diff --git a/internal/switcher/keys.go b/internal/switcher/keys.go
new file mode 100644
index 0000000..94eb6fb
--- /dev/null
+++ b/internal/switcher/keys.go
@@ -0,0 +1,33 @@
+package switcher
+
+import "github.com/charmbracelet/bubbles/key"
+
+type keybinds struct {
+ next, prev, quit key.Binding
+}
+
+func newKeybinds() keybinds {
+ return keybinds{
+ next: key.NewBinding(
+ key.WithKeys("tab"),
+ key.WithHelp("tab", "next picker"),
+ ),
+ prev: key.NewBinding(
+ key.WithKeys("shift+tab"),
+ key.WithHelp("shift+tab", "previous picker"),
+ ),
+ quit: key.NewBinding(
+ key.WithKeys("q", "ctrl+c"),
+ key.WithHelp("q", "quit"),
+ ),
+ }
+}
+
+func Keys() []key.Binding {
+ k := newKeybinds()
+ return []key.Binding{k.next, k.prev}
+}
+
+func (m Model) AllKeys() []key.Binding {
+ return append(Keys(), m.pickers[m.active].AllKeys()...)
+}
diff --git a/internal/switcher/switcher.go b/internal/switcher/switcher.go
new file mode 100644
index 0000000..d542df1
--- /dev/null
+++ b/internal/switcher/switcher.go
@@ -0,0 +1,89 @@
+package switcher
+
+import (
+ "fmt"
+
+ "github.com/charmbracelet/bubbles/key"
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/bubbletea-app-template/internal/picker"
+ "github.com/charmbracelet/bubbletea-app-template/internal/quit"
+)
+
+type Model struct {
+ active int
+ pickers []picker.Model
+}
+
+func New(pickers []picker.Model) Model {
+ return Model{
+ active: 0,
+ pickers: pickers,
+ }
+}
+
+func (m Model) fixSel(val int) int {
+ size := len(m.pickers)
+ return (val%size + size) % size
+}
+
+func (m *Model) Next() int {
+ m.active = m.fixSel(m.active + 1)
+ return m.active
+}
+
+func (m *Model) Prev() int {
+ m.active = m.fixSel(m.active - 1)
+ return m.active
+}
+
+func (m Model) Init() tea.Cmd {
+ cmds := []tea.Cmd{}
+ for _, p := range m.pickers {
+ cmds = append(cmds, p.Init())
+ }
+ return tea.Batch(cmds...)
+}
+
+func (m Model) View() string {
+ v := "|"
+ for i, p := range m.pickers {
+ if i == m.active {
+ v += fmt.Sprintf(">%s<|", p.Title())
+ } else {
+ v += fmt.Sprintf(" %s |", p.Title())
+ }
+ }
+ return fmt.Sprintf("%s\n%s", v, m.pickers[m.active].View())
+}
+
+func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ keys := newKeybinds()
+ cmds := []tea.Cmd{}
+ switch msg := msg.(type) {
+ case tea.KeyMsg:
+ switch {
+ case key.Matches(msg, keys.next):
+ cs := m.pickers[m.active].GetColor()
+ m.Next()
+ m.pickers[m.active].SetColor(cs)
+ case key.Matches(msg, keys.prev):
+ cs := m.pickers[m.active].GetColor()
+ m.Prev()
+ m.pickers[m.active].SetColor(cs)
+ case key.Matches(msg, keys.quit):
+ return quit.Model{}, tea.Quit
+ // return m, tea.Quit
+ default:
+ newActive, cmd := m.pickers[m.active].Update(msg)
+ m.pickers[m.active] = newActive.(picker.Model)
+ cmds = append(cmds, cmd)
+ return m, tea.Batch(cmds...)
+ }
+ }
+ for i, p := range m.pickers {
+ newActive, cmd := p.Update(msg)
+ m.pickers[i] = newActive.(picker.Model)
+ cmds = append(cmds, cmd)
+ }
+ return m, tea.Batch(cmds...)
+}
diff --git a/internal/util/util.go b/internal/util/util.go
new file mode 100644
index 0000000..9fea40c
--- /dev/null
+++ b/internal/util/util.go
@@ -0,0 +1,12 @@
+package util
+
+const ErrKey = "error_message"
+
+func HexMap() [16]byte {
+ return [16]byte{
+ '0', '1', '2', '3',
+ '4', '5', '6', '7',
+ '8', '9', 'a', 'b',
+ 'c', 'd', 'e', 'f',
+ }
+}