summaryrefslogtreecommitdiff
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
Semi-working POC
-rw-r--r--.gitignore2
-rw-r--r--.golangci.yml8
-rw-r--r--.goreleaser.yaml44
-rw-r--r--cmd/termpicker/flags.go5
-rw-r--r--cmd/termpicker/main.go55
-rw-r--r--go.mod34
-rw-r--r--go.sum54
-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
20 files changed, 798 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..8d7bab4
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+coverage.txt
+dist/
diff --git a/.golangci.yml b/.golangci.yml
new file mode 100644
index 0000000..ff791f8
--- /dev/null
+++ b/.golangci.yml
@@ -0,0 +1,8 @@
+linters:
+ enable:
+ - thelper
+ - gofumpt
+ - tparallel
+ - unconvert
+ - unparam
+ - wastedassign
diff --git a/.goreleaser.yaml b/.goreleaser.yaml
new file mode 100644
index 0000000..4ae2895
--- /dev/null
+++ b/.goreleaser.yaml
@@ -0,0 +1,44 @@
+# This is an example .goreleaser.yml file with some sensible defaults.
+# Make sure to check the documentation at https://goreleaser.com
+version: 2
+before:
+ hooks:
+ - go mod tidy
+
+gomod:
+ proxy: true
+
+builds:
+ - env: ["CGO_ENABLED=0"]
+ mod_timestamp: "{{ .CommitTimestamp }}"
+ flags: ["-trimpath"]
+ targets: ["go_first_class"]
+
+changelog:
+ sort: asc
+ use: github
+ filters:
+ exclude:
+ - "^docs:"
+ - "^test:"
+ - "^chore"
+ - Merge pull request
+ - Merge remote-tracking branch
+ - Merge branch
+ - go mod tidy
+ groups:
+ - title: "New Features"
+ regexp: "^.*feat[(\\w)]*:+.*$"
+ order: 0
+ - title: "Bug fixes"
+ regexp: "^.*fix[(\\w)]*:+.*$"
+ order: 10
+ - title: Other work
+ order: 999
+
+release:
+ footer: |
+
+ ---
+
+ _Released with [GoReleaser](https://goreleaser.com)!_
diff --git a/cmd/termpicker/flags.go b/cmd/termpicker/flags.go
new file mode 100644
index 0000000..f3cfd53
--- /dev/null
+++ b/cmd/termpicker/flags.go
@@ -0,0 +1,5 @@
+package main
+
+import "github.com/urfave/cli/v2"
+
+var AppFlags []cli.Flag = []cli.Flag{}
diff --git a/cmd/termpicker/main.go b/cmd/termpicker/main.go
new file mode 100644
index 0000000..31fe1c3
--- /dev/null
+++ b/cmd/termpicker/main.go
@@ -0,0 +1,55 @@
+package main
+
+import (
+ "log/slog"
+ "os"
+
+ "github.com/charmbracelet/bubbles/progress"
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/bubbletea-app-template/internal/picker"
+ "github.com/charmbracelet/bubbletea-app-template/internal/slider"
+ "github.com/charmbracelet/bubbletea-app-template/internal/switcher"
+ "github.com/charmbracelet/bubbletea-app-template/internal/util"
+ "github.com/urfave/cli/v2"
+)
+
+func AppAction(ctx *cli.Context) error {
+ // RGB {{{
+ r := slider.New('R', 255, progress.WithGradient("#660000", "#ff0000"))
+ g := slider.New('G', 255, progress.WithGradient("#006600", "#00ff00"))
+ b := slider.New('B', 255, progress.WithGradient("#000066", "#0000ff"))
+ rgb := picker.New([]slider.Model{r, g, b}, "RGB")
+ // }}}
+ // CYMK {{{
+ c := slider.New('C', 100, progress.WithGradient("#006666", "#00ffff"))
+ y := slider.New('Y', 100, progress.WithGradient("#666600", "#ffff00"))
+ m := slider.New('M', 100, progress.WithGradient("#660066", "#ff00ff"))
+ k := slider.New('K', 100, progress.WithSolidFill("#000000"))
+ cmyk := picker.New([]slider.Model{c, y, m, k}, "CMYK")
+ // }}}
+ // HSL {{{
+ h := slider.New('H', 360, progress.WithDefaultGradient())
+ s := slider.New('S', 100, progress.WithDefaultGradient())
+ l := slider.New('L', 100, progress.WithGradient("#222222", "#ffffff"))
+ hsl := picker.New([]slider.Model{h, s, l}, "HSL")
+ // }}}
+ sw := switcher.New([]picker.Model{*rgb, *cmyk, *hsl})
+ p := tea.NewProgram(sw)
+ if _, err := p.Run(); err != nil {
+ return err
+ }
+ return nil
+}
+
+func main() {
+ app := &cli.App{
+ Name: "TermPicker",
+ Usage: "A terminal-based color picker",
+ Action: AppAction,
+ Flags: AppFlags,
+ }
+ if err := app.Run(os.Args); err != nil {
+ slog.Error("Program crashed", util.ErrKey, err.Error())
+ os.Exit(1)
+ }
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..1a61455
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,34 @@
+module github.com/charmbracelet/bubbletea-app-template
+
+go 1.19
+
+require (
+ github.com/charmbracelet/bubbles v0.20.0
+ github.com/charmbracelet/bubbletea v1.2.2
+ github.com/urfave/cli/v2 v2.27.5
+)
+
+require (
+ github.com/atotto/clipboard v0.1.4 // indirect
+ github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
+ github.com/charmbracelet/harmonica v0.2.0 // indirect
+ github.com/charmbracelet/lipgloss v1.0.0 // indirect
+ github.com/charmbracelet/x/ansi v0.4.5 // indirect
+ github.com/charmbracelet/x/term v0.2.1 // indirect
+ github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
+ github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
+ github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/mattn/go-localereader v0.0.1 // indirect
+ github.com/mattn/go-runewidth v0.0.16 // indirect
+ github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
+ github.com/muesli/cancelreader v0.2.2 // indirect
+ github.com/muesli/termenv v0.15.2 // indirect
+ github.com/rivo/uniseg v0.4.7 // indirect
+ github.com/russross/blackfriday/v2 v2.1.0 // indirect
+ github.com/sahilm/fuzzy v0.1.1 // indirect
+ github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
+ golang.org/x/sync v0.9.0 // indirect
+ golang.org/x/sys v0.27.0 // indirect
+ golang.org/x/text v0.3.8 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..6232833
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,54 @@
+github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
+github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
+github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
+github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
+github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE=
+github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU=
+github.com/charmbracelet/bubbletea v1.2.2 h1:EMz//Ky/aFS2uLcKqpCst5UOE6z5CFDGRsUpyXz0chs=
+github.com/charmbracelet/bubbletea v1.2.2/go.mod h1:Qr6fVQw+wX7JkWWkVyXYk/ZUQ92a6XNekLXa3rR18MM=
+github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
+github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
+github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg=
+github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo=
+github.com/charmbracelet/x/ansi v0.4.5 h1:LqK4vwBNaXw2AyGIICa5/29Sbdq58GbGdFngSexTdRM=
+github.com/charmbracelet/x/ansi v0.4.5/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
+github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
+github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
+github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
+github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
+github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
+github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
+github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
+github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
+github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
+github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
+github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
+github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
+github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
+github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
+github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
+github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
+github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
+github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
+github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=
+github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
+github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=
+github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
+github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
+github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
+golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
+golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
+golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
+golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
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',
+ }
+}