diff --git a/.gitignore b/.gitignore index 3b735ec..b8c2a57 100644 --- a/.gitignore +++ b/.gitignore @@ -15,7 +15,7 @@ *.out # Dependency directories (remove the comment below to include it) -# vendor/ +vendor/ # Go workspace file go.work diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e69de29 diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..4cd83cd --- /dev/null +++ b/go.mod @@ -0,0 +1,42 @@ +module lukehagar/tmi + +go 1.20 + +require ( + github.com/charmbracelet/bubbles v0.16.1 + github.com/charmbracelet/bubbletea v0.24.2 + github.com/charmbracelet/glamour v0.6.0 + github.com/charmbracelet/lipgloss v0.7.1 + github.com/spf13/cobra v1.7.0 +) + +require ( + github.com/alecthomas/chroma v0.10.0 // indirect + github.com/atotto/clipboard v0.1.4 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/aymerick/douceur v0.2.0 // indirect + github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect + github.com/dlclark/regexp2 v1.4.0 // indirect + github.com/gorilla/css v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.18 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.14 // indirect + github.com/microcosm-cc/bluemonday v1.0.21 // indirect + github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/reflow v0.3.0 // indirect + github.com/muesli/termenv v0.15.1 // indirect + github.com/olekukonko/tablewriter v0.0.5 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + github.com/sahilm/fuzzy v0.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/yuin/goldmark v1.5.2 // indirect + github.com/yuin/goldmark-emoji v1.0.1 // indirect + golang.org/x/net v0.0.0-20221002022538-bcab6841153b // indirect + golang.org/x/sync v0.1.0 // indirect + golang.org/x/sys v0.6.0 // indirect + golang.org/x/term v0.6.0 // indirect + golang.org/x/text v0.3.8 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..c41c27e --- /dev/null +++ b/go.sum @@ -0,0 +1,95 @@ +github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek= +github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s= +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 v1.0.3/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4= +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/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= +github.com/charmbracelet/bubbles v0.16.1 h1:6uzpAAaT9ZqKssntbvZMlksWHruQLNxg49H5WdeuYSY= +github.com/charmbracelet/bubbles v0.16.1/go.mod h1:2QCp9LFlEsBQMvIYERr7Ww2H2bA7xen1idUDIzm/+Xc= +github.com/charmbracelet/bubbletea v0.24.2 h1:uaQIKx9Ai6Gdh5zpTbGiWpytMU+CfsPp06RaW2cx/SY= +github.com/charmbracelet/bubbletea v0.24.2/go.mod h1:XdrNrV4J8GiyshTtx3DNuYkR1FDaJmO3l2nejekbsgg= +github.com/charmbracelet/glamour v0.6.0 h1:wi8fse3Y7nfcabbbDuwolqTqMQPMnVPeZhDM273bISc= +github.com/charmbracelet/glamour v0.6.0/go.mod h1:taqWV4swIMMbWALc0m7AfE9JkPSU8om2538k9ITBxOc= +github.com/charmbracelet/lipgloss v0.7.1 h1:17WMwi7N1b1rVWOjMT+rCh7sQkvDU75B2hbZpc5Kc1E= +github.com/charmbracelet/lipgloss v0.7.1/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNWpFujkNawKNhE2c= +github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= +github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E= +github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= +github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +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.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= +github.com/mattn/go-isatty v0.0.18/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.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= +github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/microcosm-cc/bluemonday v1.0.21 h1:dNH3e4PSyE4vNX+KlRGHT5KrSvjeUkoNPwEORjffHJg= +github.com/microcosm-cc/bluemonday v1.0.21/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM= +github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34= +github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= +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/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= +github.com/muesli/termenv v0.13.0/go.mod h1:sP1+uffeLaEYpyOTb8pLCUctGcGLnoFjSn4YJK5e2bc= +github.com/muesli/termenv v0.15.1 h1:UzuTb/+hhlBugQz28rpzey4ZuKcZ03MeKsoG7IJZIxs= +github.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI= +github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.5.2 h1:ALmeCk/px5FSm1MAcFBAsVKZjDuMVj8Tm7FFIlMJnqU= +github.com/yuin/goldmark v1.5.2/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/goldmark-emoji v1.0.1 h1:ctuWEyzGBwiucEqxzwe0SOYDXPAucOrE9NQC18Wa1os= +github.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGjRIBbIZQFqkQ= +golang.org/x/net v0.0.0-20221002022538-bcab6841153b h1:6e93nYa3hNqAvLr0pD4PN1fFS+gKzp2zAXqrnTCstqU= +golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..fc17833 --- /dev/null +++ b/main.go @@ -0,0 +1,18 @@ +/* +Copyright © 2023 NAME HERE +*/ +package main + +import ( + "lukehagar/tmi/root" + "os" +) + +func main() { + rootCmd := root.NewRootCmd() + + err := rootCmd.Execute() + if err != nil { + os.Exit(1) + } +} diff --git a/root/example.md b/root/example.md new file mode 100644 index 0000000..f546c51 --- /dev/null +++ b/root/example.md @@ -0,0 +1,3 @@ +```bash +tmi +``` \ No newline at end of file diff --git a/root/long.md b/root/long.md new file mode 100644 index 0000000..034dc9c --- /dev/null +++ b/root/long.md @@ -0,0 +1,3 @@ +# Terminal Music Interface + +Test \ No newline at end of file diff --git a/root/root.go b/root/root.go new file mode 100644 index 0000000..5ffa8b8 --- /dev/null +++ b/root/root.go @@ -0,0 +1,58 @@ +/* +Copyright © 2023 Luke Hagar lukeslakemail@gmail.com +*/ + +package root + +import ( + _ "embed" + "fmt" + "log" + "lukehagar/tmi/root/tui" + "math/rand" + "os" + "time" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/glamour" + "github.com/spf13/cobra" +) + +//go:embed long.md +var LongMD string + +//go:embed example.md +var ExampleMD string + +func RenderMarkdown(in string) string { + renderer, _ := glamour.NewTermRenderer( + glamour.WithAutoStyle(), + ) + + out, err := renderer.Render(in) + if err != nil { + log.Panic(err) + } + return out +} + +func NewRootCmd() *cobra.Command { + var rootCmd = &cobra.Command{ + Use: "tmi", + Short: "A Terminal Music Interface", + Long: RenderMarkdown(LongMD), + Example: RenderMarkdown(ExampleMD), + RunE: func(cmd *cobra.Command, args []string) error { + + rand.Seed(time.Now().UTC().UnixNano()) + + if _, err := tea.NewProgram(tui.NewModel()).Run(); err != nil { + fmt.Println("Error running program:", err) + os.Exit(1) + } + + return nil + }, + } + return rootCmd +} diff --git a/root/tui/delegate.go b/root/tui/delegate.go new file mode 100644 index 0000000..6c3307b --- /dev/null +++ b/root/tui/delegate.go @@ -0,0 +1,89 @@ +package tui + +import ( + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" +) + +func newItemDelegate(keys *delegateKeyMap) list.DefaultDelegate { + d := list.NewDefaultDelegate() + + d.UpdateFunc = func(msg tea.Msg, m *list.Model) tea.Cmd { + var title string + + if i, ok := m.SelectedItem().(item); ok { + title = i.Title() + } else { + return nil + } + + switch msg := msg.(type) { + case tea.KeyMsg: + switch { + case key.Matches(msg, keys.choose): + return m.NewStatusMessage(statusMessageStyle("You chose " + title)) + + case key.Matches(msg, keys.remove): + index := m.Index() + m.RemoveItem(index) + if len(m.Items()) == 0 { + keys.remove.SetEnabled(false) + } + return m.NewStatusMessage(statusMessageStyle("Deleted " + title)) + } + } + + return nil + } + + help := []key.Binding{keys.choose, keys.remove} + + d.ShortHelpFunc = func() []key.Binding { + return help + } + + d.FullHelpFunc = func() [][]key.Binding { + return [][]key.Binding{help} + } + + return d +} + +type delegateKeyMap struct { + choose key.Binding + remove key.Binding +} + +// Additional short help entries. This satisfies the help.KeyMap interface and +// is entirely optional. +func (d delegateKeyMap) ShortHelp() []key.Binding { + return []key.Binding{ + d.choose, + d.remove, + } +} + +// Additional full help entries. This satisfies the help.KeyMap interface and +// is entirely optional. +func (d delegateKeyMap) FullHelp() [][]key.Binding { + return [][]key.Binding{ + { + d.choose, + d.remove, + }, + } +} + +func newDelegateKeyMap() *delegateKeyMap { + return &delegateKeyMap{ + choose: key.NewBinding( + key.WithKeys("enter"), + key.WithHelp("enter", "choose"), + ), + remove: key.NewBinding( + key.WithKeys("x", "backspace"), + key.WithHelp("x", "delete"), + ), + } +} diff --git a/root/tui/randomitems.go b/root/tui/randomitems.go new file mode 100644 index 0000000..89d25f9 --- /dev/null +++ b/root/tui/randomitems.go @@ -0,0 +1,164 @@ +package tui + +import ( + "math/rand" + "sync" +) + +type randomItemGenerator struct { + titles []string + descs []string + titleIndex int + descIndex int + mtx *sync.Mutex + shuffle *sync.Once +} + +func (r *randomItemGenerator) reset() { + r.mtx = &sync.Mutex{} + r.shuffle = &sync.Once{} + + r.titles = []string{ + "Artichoke", + "Baking Flour", + "Bananas", + "Barley", + "Bean Sprouts", + "Bitter Melon", + "Black Cod", + "Blood Orange", + "Brown Sugar", + "Cashew Apple", + "Cashews", + "Cat Food", + "Coconut Milk", + "Cucumber", + "Curry Paste", + "Currywurst", + "Dill", + "Dragonfruit", + "Dried Shrimp", + "Eggs", + "Fish Cake", + "Furikake", + "Garlic", + "Gherkin", + "Ginger", + "Granulated Sugar", + "Grapefruit", + "Green Onion", + "Hazelnuts", + "Heavy whipping cream", + "Honey Dew", + "Horseradish", + "Jicama", + "Kohlrabi", + "Leeks", + "Lentils", + "Licorice Root", + "Meyer Lemons", + "Milk", + "Molasses", + "Muesli", + "Nectarine", + "Niagamo Root", + "Nopal", + "Nutella", + "Oat Milk", + "Oatmeal", + "Olives", + "Papaya", + "Party Gherkin", + "Peppers", + "Persian Lemons", + "Pickle", + "Pineapple", + "Plantains", + "Pocky", + "Powdered Sugar", + "Quince", + "Radish", + "Ramps", + "Star Anise", + "Sweet Potato", + "Tamarind", + "Unsalted Butter", + "Watermelon", + "Weißwurst", + "Yams", + "Yeast", + "Yuzu", + "Snow Peas", + } + + r.descs = []string{ + "A little weird", + "Bold flavor", + "Can’t get enough", + "Delectable", + "Expensive", + "Expired", + "Exquisite", + "Fresh", + "Gimme", + "In season", + "Kind of spicy", + "Looks fresh", + "Looks good to me", + "Maybe not", + "My favorite", + "Oh my", + "On sale", + "Organic", + "Questionable", + "Really fresh", + "Refreshing", + "Salty", + "Scrumptious", + "Delectable", + "Slightly sweet", + "Smells great", + "Tasty", + "Too ripe", + "At last", + "What?", + "Wow", + "Yum", + "Maybe", + "Sure, why not?", + } + + r.shuffle.Do(func() { + shuf := func(x []string) { + rand.Shuffle(len(x), func(i, j int) { x[i], x[j] = x[j], x[i] }) + } + shuf(r.titles) + shuf(r.descs) + }) +} + +func (r *randomItemGenerator) next() item { + if r.mtx == nil { + r.reset() + } + + r.mtx.Lock() + defer r.mtx.Unlock() + + i := item{ + title: r.titles[r.titleIndex], + description: r.descs[r.descIndex], + } + + r.titleIndex++ + if r.titleIndex >= len(r.titles) { + r.titleIndex = 0 + } + + r.descIndex++ + if r.descIndex >= len(r.descs) { + r.descIndex = 0 + } + + return i +} diff --git a/root/tui/tui.go b/root/tui/tui.go new file mode 100644 index 0000000..8073144 --- /dev/null +++ b/root/tui/tui.go @@ -0,0 +1,176 @@ +package tui + +import ( + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +var ( + appStyle = lipgloss.NewStyle().Padding(1, 2) + + titleStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("#FFFDF5")). + Background(lipgloss.Color("#25A065")). + Padding(0, 1) + + statusMessageStyle = lipgloss.NewStyle(). + Foreground(lipgloss.AdaptiveColor{Light: "#04B575", Dark: "#04B575"}). + Render +) + +type item struct { + title string + description string +} + +func (i item) Title() string { return i.title } +func (i item) Description() string { return i.description } +func (i item) FilterValue() string { return i.title } + +type listKeyMap struct { + toggleSpinner key.Binding + toggleTitleBar key.Binding + toggleStatusBar key.Binding + togglePagination key.Binding + toggleHelpMenu key.Binding + insertItem key.Binding +} + +func newListKeyMap() *listKeyMap { + return &listKeyMap{ + insertItem: key.NewBinding( + key.WithKeys("a"), + key.WithHelp("a", "add item"), + ), + toggleSpinner: key.NewBinding( + key.WithKeys("s"), + key.WithHelp("s", "toggle spinner"), + ), + toggleTitleBar: key.NewBinding( + key.WithKeys("T"), + key.WithHelp("T", "toggle title"), + ), + toggleStatusBar: key.NewBinding( + key.WithKeys("S"), + key.WithHelp("S", "toggle status"), + ), + togglePagination: key.NewBinding( + key.WithKeys("P"), + key.WithHelp("P", "toggle pagination"), + ), + toggleHelpMenu: key.NewBinding( + key.WithKeys("H"), + key.WithHelp("H", "toggle help"), + ), + } +} + +type model struct { + list list.Model + itemGenerator *randomItemGenerator + keys *listKeyMap + delegateKeys *delegateKeyMap +} + +func NewModel() model { + var ( + itemGenerator randomItemGenerator + delegateKeys = newDelegateKeyMap() + listKeys = newListKeyMap() + ) + + // Make initial list of items + const numItems = 24 + items := make([]list.Item, numItems) + for i := 0; i < numItems; i++ { + items[i] = itemGenerator.next() + } + + // Setup list + delegate := newItemDelegate(delegateKeys) + groceryList := list.New(items, delegate, 0, 0) + groceryList.Title = "Groceries" + groceryList.Styles.Title = titleStyle + groceryList.AdditionalFullHelpKeys = func() []key.Binding { + return []key.Binding{ + listKeys.toggleSpinner, + listKeys.insertItem, + listKeys.toggleTitleBar, + listKeys.toggleStatusBar, + listKeys.togglePagination, + listKeys.toggleHelpMenu, + } + } + + return model{ + list: groceryList, + keys: listKeys, + delegateKeys: delegateKeys, + itemGenerator: &itemGenerator, + } +} + +func (m model) Init() tea.Cmd { + return tea.EnterAltScreen +} + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmds []tea.Cmd + + switch msg := msg.(type) { + case tea.WindowSizeMsg: + h, v := appStyle.GetFrameSize() + m.list.SetSize(msg.Width-h, msg.Height-v) + + case tea.KeyMsg: + // Don't match any of the keys below if we're actively filtering. + if m.list.FilterState() == list.Filtering { + break + } + + switch { + case key.Matches(msg, m.keys.toggleSpinner): + cmd := m.list.ToggleSpinner() + return m, cmd + + case key.Matches(msg, m.keys.toggleTitleBar): + v := !m.list.ShowTitle() + m.list.SetShowTitle(v) + m.list.SetShowFilter(v) + m.list.SetFilteringEnabled(v) + return m, nil + + case key.Matches(msg, m.keys.toggleStatusBar): + m.list.SetShowStatusBar(!m.list.ShowStatusBar()) + return m, nil + + case key.Matches(msg, m.keys.togglePagination): + m.list.SetShowPagination(!m.list.ShowPagination()) + return m, nil + + case key.Matches(msg, m.keys.toggleHelpMenu): + m.list.SetShowHelp(!m.list.ShowHelp()) + return m, nil + + case key.Matches(msg, m.keys.insertItem): + m.delegateKeys.remove.SetEnabled(true) + newItem := m.itemGenerator.next() + insCmd := m.list.InsertItem(0, newItem) + statusCmd := m.list.NewStatusMessage(statusMessageStyle("Added " + newItem.Title())) + return m, tea.Batch(insCmd, statusCmd) + } + } + + // This will also call our delegate's update function. + newListModel, cmd := m.list.Update(msg) + m.list = newListModel + cmds = append(cmds, cmd) + + return m, tea.Batch(cmds...) +} + +func (m model) View() string { + return appStyle.Render(m.list.View()) +} diff --git a/tmi b/tmi new file mode 100755 index 0000000..3dfccaf Binary files /dev/null and b/tmi differ