diff options
author | Benjamin Chausse <benjamin@chausse.xyz> | 2024-11-23 18:12:03 -0500 |
---|---|---|
committer | Benjamin Chausse <benjamin@chausse.xyz> | 2024-11-23 18:12:03 -0500 |
commit | 89094fecf4cb1c018f15c976641cd18c255eac28 (patch) | |
tree | 9f6e32c38013bc526399ab324891e0b3269e50dc |
Semi-working POC
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | .golangci.yml | 8 | ||||
-rw-r--r-- | .goreleaser.yaml | 44 | ||||
-rw-r--r-- | cmd/termpicker/flags.go | 5 | ||||
-rw-r--r-- | cmd/termpicker/main.go | 55 | ||||
-rw-r--r-- | go.mod | 34 | ||||
-rw-r--r-- | go.sum | 54 | ||||
-rw-r--r-- | internal/colors/cmyk.go | 46 | ||||
-rw-r--r-- | internal/colors/hsl.go | 1 | ||||
-rw-r--r-- | internal/colors/precise.go | 38 | ||||
-rw-r--r-- | internal/colors/rgb.go | 25 | ||||
-rw-r--r-- | internal/picker/keys.go | 29 | ||||
-rw-r--r-- | internal/picker/picker.go | 137 | ||||
-rw-r--r-- | internal/quit/quit.go | 17 | ||||
-rw-r--r-- | internal/slider/evaluation.go | 46 | ||||
-rw-r--r-- | internal/slider/keys.go | 52 | ||||
-rw-r--r-- | internal/slider/slider.go | 71 | ||||
-rw-r--r-- | internal/switcher/keys.go | 33 | ||||
-rw-r--r-- | internal/switcher/switcher.go | 89 | ||||
-rw-r--r-- | internal/util/util.go | 12 |
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) + } +} @@ -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 +) @@ -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', + } +} |