├── .github ├── dependabot.yml └── workflows │ ├── build.yml │ ├── lint.yml │ └── release.yml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yaml ├── LICENSE ├── README.md ├── constants └── constants.go ├── form.go ├── go.mod ├── go.sum ├── main.go ├── model.go ├── task.go └── todo.md /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | time: "08:00" 8 | labels: 9 | - "dependencies" 10 | - package-ecosystem: "github-actions" 11 | directory: "/" 12 | schedule: 13 | interval: "daily" 14 | time: "08:00" 15 | labels: 16 | - "dependencies" 17 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: actions/setup-go@v3 16 | with: 17 | go-version: ~1.18 18 | cache: true 19 | - name: test 20 | run: | 21 | go test -v -failfast -race -coverpkg=./... -covermode=atomic -coverprofile=coverage.txt ./... 22 | - uses: codecov/codecov-action@v3 23 | if: matrix.os == 'ubuntu-latest' 24 | with: 25 | token: ${{ secrets.CODECOV_TOKEN }} 26 | file: ./coverage.txt 27 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | on: 3 | push: 4 | pull_request: 5 | 6 | permissions: 7 | contents: read 8 | 9 | jobs: 10 | golangci: 11 | name: lint 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: actions/setup-go@v3 16 | with: 17 | go-version: ~1.18 18 | cache: true 19 | - name: golangci-lint 20 | uses: golangci/golangci-lint-action@v3 21 | with: 22 | skip-go-installation: true 23 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | pull_request: 8 | 9 | permissions: 10 | contents: write 11 | id-token: write 12 | packages: write 13 | 14 | jobs: 15 | goreleaser: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v3 19 | with: 20 | fetch-depth: 0 21 | - uses: actions/setup-go@v3 22 | with: 23 | go-version: ~1.18 24 | cache: true 25 | - uses: goreleaser/goreleaser-action@v3 26 | with: 27 | version: latest 28 | args: release --rm-dist 29 | env: 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage.txt 2 | debug.log 3 | dist/ 4 | *.log 5 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | enable: 3 | - thelper 4 | - gofumpt 5 | - tparallel 6 | - unconvert 7 | - unparam 8 | - wastedassign 9 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | # This is an example .goreleaser.yml file with some sensible defaults. 2 | # Make sure to check the documentation at https://goreleaser.com 3 | before: 4 | hooks: 5 | - go mod tidy 6 | 7 | gomod: 8 | proxy: true 9 | 10 | builds: 11 | - env: ["CGO_ENABLED=0"] 12 | mod_timestamp: '{{ .CommitTimestamp }}' 13 | flags: ["-trimpath"] 14 | ldflags: ["-s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{ .CommitDate }}"] 15 | targets: ["go_first_class"] 16 | 17 | changelog: 18 | sort: asc 19 | use: github 20 | filters: 21 | exclude: 22 | - '^docs:' 23 | - '^test:' 24 | - '^chore' 25 | - Merge pull request 26 | - Merge remote-tracking branch 27 | - Merge branch 28 | - go mod tidy 29 | groups: 30 | - title: 'New Features' 31 | regexp: "^.*feat[(\\w)]*:+.*$" 32 | order: 0 33 | - title: 'Bug fixes' 34 | regexp: "^.*fix[(\\w)]*:+.*$" 35 | order: 10 36 | - title: Other work 37 | order: 999 38 | 39 | release: 40 | footer: | 41 | 42 | --- 43 | 44 | _Released with [GoReleaser](https://goreleaser.com)!_ 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Charmbracelet, Inc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bubbletea-app-template 2 | 3 | A template repository to create [Bubbletea][bubbletea] apps. 4 | 5 | ## Included 6 | 7 | - a sample app that does nothing, so it includes all dependencies: 8 | - [bubbletea][] 9 | - [bubbles][] 10 | - [lipgloss][] 11 | - github actions workflows for build, test, lint and release 12 | - [GoReleaser][goreleaser] configs 13 | - [golangci-lint][lint] configs 14 | 15 | [bubbletea]: https://github.com/charmbracelet/bubbletea 16 | [bubbles]: https://github.com/charmbracelet/bubbles 17 | [lipgloss]: https://github.com/charmbracelet/lipgloss 18 | [goreleaser]: https://goreleaser.com 19 | [lint]: https://golangci-lint.run 20 | -------------------------------------------------------------------------------- /constants/constants.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | import "github.com/charmbracelet/bubbles/key" 4 | 5 | type ( 6 | ErrMsg error 7 | ) 8 | 9 | var QuitKeys = key.NewBinding( 10 | key.WithKeys("q", "esc", "ctrl+c"), 11 | key.WithHelp("", "press q to quit"), 12 | ) 13 | -------------------------------------------------------------------------------- /form.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/bashbunni/kancli/constants" 8 | "github.com/charmbracelet/bubbles/key" 9 | "github.com/charmbracelet/bubbles/textarea" 10 | "github.com/charmbracelet/bubbles/textinput" 11 | tea "github.com/charmbracelet/bubbletea" 12 | "github.com/charmbracelet/lipgloss" 13 | ) 14 | 15 | type Form struct { 16 | status status 17 | title textinput.Model 18 | description textarea.Model 19 | } 20 | 21 | func newForm(state status) *Form { 22 | form := &Form{status: state, description: textarea.New()} 23 | form.title = textinput.New() 24 | form.title.Focus() 25 | return form 26 | } 27 | 28 | func (m Form) Init() tea.Cmd { 29 | return textinput.Blink 30 | } 31 | 32 | func (m Form) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 33 | var cmd tea.Cmd 34 | switch msg := msg.(type) { 35 | case tea.KeyMsg: 36 | if key.Matches(msg, constants.QuitKeys) { 37 | return m, tea.Quit 38 | } 39 | switch msg.String() { 40 | case "enter": 41 | if m.title.Focused() { 42 | m.title.Blur() 43 | m.description.Focus() 44 | return m, textarea.Blink 45 | } else { 46 | // switch to previous model, add task 47 | models[input] = m 48 | return models[tasks], m.NewTask 49 | } 50 | } 51 | } 52 | if m.title.Focused() { 53 | m.title, cmd = m.title.Update(msg) 54 | return m, cmd 55 | } else { 56 | m.description, cmd = m.description.Update(msg) 57 | return m, cmd 58 | } 59 | } 60 | 61 | func (m Form) NewTask() tea.Msg { 62 | task := Task{status: m.status, title: m.title.Value(), description: m.description.Value()} 63 | log.Print(task) 64 | return task 65 | } 66 | 67 | func (m Form) helpMenu() string { 68 | var msg string 69 | if m.title.Focused() { 70 | msg = "next" 71 | } else { 72 | msg = "submit" 73 | } 74 | return helpStyle.Render(fmt.Sprintf("enter: %s", msg)) 75 | } 76 | 77 | func (m Form) View() string { 78 | return lipgloss.JoinVertical(lipgloss.Left, m.title.View(), m.description.View(), m.helpMenu()) 79 | } 80 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/bashbunni/kancli 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/charmbracelet/bubbles v0.13.0 7 | github.com/charmbracelet/bubbletea v0.22.0 8 | github.com/charmbracelet/lipgloss v0.5.0 9 | ) 10 | 11 | require ( 12 | github.com/atotto/clipboard v0.1.4 // indirect 13 | github.com/containerd/console v1.0.3 // indirect 14 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 15 | github.com/mattn/go-isatty v0.0.14 // indirect 16 | github.com/mattn/go-runewidth v0.0.13 // indirect 17 | github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect 18 | github.com/muesli/cancelreader v0.2.1 // indirect 19 | github.com/muesli/reflow v0.3.0 // indirect 20 | github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 // indirect 21 | github.com/rivo/uniseg v0.2.0 // indirect 22 | github.com/sahilm/fuzzy v0.1.0 // indirect 23 | golang.org/x/sys v0.0.0-20220209214540-3681064d5158 // indirect 24 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect 25 | ) 26 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= 2 | github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= 3 | github.com/charmbracelet/bubbles v0.13.0 h1:zP/ROH3wJEBqZWKIsD50ZKKlx3ydLInq3LdD/Nrlb8w= 4 | github.com/charmbracelet/bubbles v0.13.0/go.mod h1:bbeTiXwPww4M031aGi8UK2HT9RDWoiNibae+1yCMtcc= 5 | github.com/charmbracelet/bubbletea v0.21.0/go.mod h1:GgmJMec61d08zXsOhqRC/AiOx4K4pmz+VIcRIm1FKr4= 6 | github.com/charmbracelet/bubbletea v0.22.0 h1:E1BTNSE3iIrq0G0X6TjGAmrQ32cGCbFDPcIuImikrUc= 7 | github.com/charmbracelet/bubbletea v0.22.0/go.mod h1:aoVIwlNlr5wbCB26KhxfrqAn0bMp4YpJcoOelbxApjs= 8 | github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= 9 | github.com/charmbracelet/lipgloss v0.5.0 h1:lulQHuVeodSgDez+3rGiuxlPVXSnhth442DATR2/8t8= 10 | github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs= 11 | github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= 12 | github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= 13 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 14 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 15 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 16 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 17 | github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= 18 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 19 | github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= 20 | github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= 21 | github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= 22 | github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 23 | github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34= 24 | github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= 25 | github.com/muesli/cancelreader v0.2.0/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= 26 | github.com/muesli/cancelreader v0.2.1 h1:Xzd1B4U5bWQOuSKuN398MyynIGTNT89dxzpEDsalXZs= 27 | github.com/muesli/cancelreader v0.2.1/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= 28 | github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ= 29 | github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= 30 | github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= 31 | github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs= 32 | github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 h1:QANkGiGr39l1EESqrE0gZw0/AJNYzIvoGLhIoVYtluI= 33 | github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs= 34 | github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 35 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= 36 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 37 | github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI= 38 | github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= 39 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 40 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 41 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 42 | golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 43 | golang.org/x/sys v0.0.0-20220209214540-3681064d5158 h1:rm+CHSpPEEW2IsXUib1ThaHIjuBVZjxNgSKmBLFfD4c= 44 | golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 45 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= 46 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 47 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | /* 4 | Qs 5 | - don't need to store list of Items and data structure - should be able to wrap each of them when necessary 6 | - cast it as type, modify it, sub it back in as interface 7 | */ 8 | 9 | /* functionality 10 | - add tasks to current list 11 | - edit selected task 12 | */ 13 | 14 | import ( 15 | "fmt" 16 | "os" 17 | 18 | tea "github.com/charmbracelet/bubbletea" 19 | "github.com/charmbracelet/lipgloss" 20 | ) 21 | 22 | type ( 23 | status uint 24 | page int 25 | ) 26 | 27 | const ( 28 | todo status = iota 29 | inProgress 30 | done 31 | ) 32 | 33 | const ( 34 | tasks page = iota 35 | input 36 | ) 37 | 38 | const ( 39 | divisor = 4 40 | ) 41 | 42 | var ( 43 | models []tea.Model 44 | columnStyle = lipgloss.NewStyle(). 45 | Padding(1, 2). 46 | Border(lipgloss.HiddenBorder()) 47 | focusedStyle = lipgloss.NewStyle(). 48 | Padding(1, 2). 49 | Border(lipgloss.RoundedBorder()). 50 | BorderForeground(lipgloss.Color("62")) 51 | helpStyle = lipgloss.NewStyle(). 52 | Foreground(lipgloss.Color("241")) 53 | ) 54 | 55 | func main() { 56 | models = []tea.Model{newModel(), newForm(todo)} 57 | 58 | p := tea.NewProgram(models[tasks]) 59 | if err := p.Start(); err != nil { 60 | fmt.Println(err) 61 | os.Exit(1) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /model.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/bashbunni/kancli/constants" 7 | "github.com/charmbracelet/bubbles/key" 8 | "github.com/charmbracelet/bubbles/list" 9 | tea "github.com/charmbracelet/bubbletea" 10 | "github.com/charmbracelet/lipgloss" 11 | ) 12 | 13 | type Model struct { 14 | focus status 15 | loaded bool 16 | lists []list.Model 17 | quitting bool 18 | } 19 | 20 | func (m *Model) Next() { 21 | if m.focus == done { 22 | m.focus = todo 23 | } else { 24 | m.focus++ 25 | } 26 | } 27 | 28 | func (m *Model) Prev() { 29 | if m.focus == todo { 30 | m.focus = done 31 | } else { 32 | m.focus-- 33 | } 34 | } 35 | 36 | func newModel() Model { 37 | m := Model{focus: todo, loaded: false} 38 | return m 39 | } 40 | 41 | func (m *Model) initLists(width, height int) { 42 | // init list model 43 | defaultList := list.New([]list.Item{}, list.NewDefaultDelegate(), width/divisor, height-divisor*2) 44 | defaultList.SetShowHelp(false) 45 | m.lists = []list.Model{defaultList, defaultList, defaultList} 46 | // add list items 47 | m.lists[todo].SetItems([]list.Item{ 48 | Task{status: todo, title: "buy milk", description: "strawberry milk"}, 49 | Task{status: todo, title: "eat sushi", description: "negitoro roll, miso soup, rice"}, 50 | Task{status: todo, title: "fold laundry", description: "or wear wrinkly t-shirts"}, 51 | }) 52 | m.lists[todo].Title = "To Do" 53 | m.lists[inProgress].SetItems([]list.Item{ 54 | Task{status: inProgress, title: "write code", description: "don't worry, it's go"}, 55 | }) 56 | m.lists[inProgress].Title = "In Progress" 57 | m.lists[done].SetItems([]list.Item{ 58 | Task{status: done, title: "stay cool", description: "as a cucumber"}, 59 | }) 60 | m.lists[done].Title = "Done" 61 | } 62 | 63 | func (m Model) Init() tea.Cmd { 64 | return nil 65 | } 66 | 67 | func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 68 | var cmd tea.Cmd 69 | switch msg := msg.(type) { 70 | // TODO: check if this is where they put custom messages in other examples 71 | case tea.WindowSizeMsg: 72 | if !m.loaded { 73 | columnStyle.Width(msg.Width / divisor) 74 | focusedStyle.Width(msg.Width / divisor) 75 | m.initLists(msg.Width, msg.Height) 76 | m.loaded = true 77 | } 78 | case tea.KeyMsg: 79 | if key.Matches(msg, constants.QuitKeys) { 80 | m.quitting = true 81 | return m, tea.Quit 82 | } 83 | switch msg.String() { 84 | case "right": 85 | m.Next() 86 | case "left": 87 | m.Prev() 88 | case "enter": 89 | return m, m.MoveToNext 90 | case "n": 91 | // save state of current model before switching models 92 | models[tasks] = m 93 | models[input] = newForm(m.focus) 94 | return models[input].Update(nil) 95 | // Note: I don't need a list of models, I can just return a new 96 | // form model each time, but I'm keeping it in this case so you can 97 | // see what it looks like with a list of models } 98 | } 99 | case Task: 100 | task := msg 101 | log.Println(task.status) 102 | return m, m.lists[task.status].InsertItem(len(m.lists[task.status].Items()), task) 103 | } 104 | currList, cmd := m.lists[m.focus].Update(msg) 105 | m.lists[m.focus] = currList 106 | return m, cmd 107 | } 108 | 109 | func (m *Model) MoveToNext() tea.Msg { 110 | selectedItem := m.lists[m.focus].SelectedItem() 111 | selectedTask := selectedItem.(Task) 112 | m.lists[selectedTask.status].RemoveItem(m.lists[m.focus].Index()) 113 | selectedTask.Next() 114 | m.lists[selectedTask.status].InsertItem(len(m.lists[selectedTask.status].Items())-1, list.Item(selectedTask)) 115 | return nil 116 | } 117 | 118 | func (m Model) View() string { 119 | var cols []string 120 | if m.quitting { 121 | return "" 122 | } 123 | if m.loaded { 124 | todoView := m.lists[todo].View() 125 | inProgView := m.lists[inProgress].View() 126 | doneView := m.lists[done].View() 127 | switch m.focus { 128 | case inProgress: 129 | cols = []string{ 130 | columnStyle.Render(todoView), 131 | focusedStyle.Render(inProgView), 132 | columnStyle.Render(doneView), 133 | } 134 | case done: 135 | cols = []string{ 136 | columnStyle.Render(todoView), 137 | columnStyle.Render(inProgView), 138 | focusedStyle.Render(doneView), 139 | } 140 | default: 141 | cols = []string{ 142 | focusedStyle.Render(todoView), 143 | columnStyle.Render(inProgView), 144 | columnStyle.Render(doneView), 145 | } 146 | } 147 | return lipgloss.JoinHorizontal(lipgloss.Left, cols...) 148 | } else { 149 | return "Loading..." 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /task.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type Task struct { 4 | status status 5 | title string 6 | description string 7 | } 8 | 9 | func (t Task) FilterValue() string { 10 | return t.title 11 | } 12 | 13 | func (t Task) Title() string { 14 | return t.title 15 | } 16 | 17 | func (t Task) Description() string { 18 | return t.description 19 | } 20 | 21 | func (t *Task) Next() { 22 | if t.status == done { 23 | t.status = todo 24 | } else { 25 | t.status++ 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /todo.md: -------------------------------------------------------------------------------- 1 | # TODOs 2 | 3 | - add tasks to current list 4 | - edit selected task 5 | - [x] move selected task to next board 6 | 7 | ## Layout 8 | 9 | todo | in progress | done 10 | 11 | - manage state 12 | --------------------------------------------------------------------------------