├── .gitignore ├── LICENSE ├── README.md ├── app.go ├── go.mod ├── go.sum ├── main.go └── models ├── idea.go └── submit.go /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 nore-dev 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 |
2 |
3 | 4 |

What CLI

5 | 6 | **[What to code](https://what-to-code.com) CLI client** 7 |
8 | 9 | ![](https://img.shields.io/github/license/nore-dev/gh-timeline) 10 | ## About The Project 11 | 12 | 13 | 14 | 15 | This project allows you to view ideas from [**what-to-code.com**](https://what-to-code.com) from your terminal. You can also share your own ideas and like the ideas you like. 16 | 17 | ![](https://i.imgur.com/5l4hUZa.png) 18 | 19 | ## Built With 20 | * [Golang](https://go.dev/) 21 | * [Bubble Tea](https://github.com/charmbracelet/bubbletea) 22 | ## Contributing 23 | 24 | Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. 25 | 26 | If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement". 27 | Don't forget to give the project a star! Thanks again! 28 | 29 | 1. Fork the Project 30 | 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) 31 | 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) 32 | 4. Push to the Branch (`git push origin feature/AmazingFeature`) 33 | 5. Open a Pull Request 34 | 35 | 36 | ## LICENSE 37 | 38 | Distributed under the **MIT** License. See `LICENSE.txt` for more information. 39 | 40 |

(back to top)

41 | -------------------------------------------------------------------------------- /app.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | 7 | "github.com/charmbracelet/bubbles/list" 8 | tea "github.com/charmbracelet/bubbletea" 9 | "github.com/charmbracelet/lipgloss" 10 | "github.com/nore-dev/what-cli/models" 11 | ) 12 | 13 | type App struct { 14 | list list.Model 15 | ideaModel models.IdeaModel 16 | submitModel models.SubmitModel 17 | selected bool 18 | isIdeaPage bool 19 | } 20 | 21 | var ( 22 | itemStyle = lipgloss.NewStyle().PaddingLeft(4) 23 | selectedItemStyle = lipgloss.NewStyle().PaddingLeft(2).Foreground(lipgloss.Color("170")) 24 | paginationStyle = list.DefaultStyles().PaginationStyle.PaddingLeft(4) 25 | helpStyle = list.DefaultStyles().HelpStyle.PaddingLeft(4).PaddingBottom(1) 26 | ) 27 | 28 | type Page string 29 | 30 | func (p Page) FilterValue() string { return "" } 31 | 32 | type itemDelegate struct{} 33 | 34 | func (d itemDelegate) Height() int { return 1 } 35 | func (d itemDelegate) Spacing() int { return 0 } 36 | func (d itemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd { 37 | return nil 38 | } 39 | 40 | func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) { 41 | i, ok := listItem.(Page) 42 | if !ok { 43 | return 44 | } 45 | 46 | fn := itemStyle.Render 47 | if index == m.Index() { 48 | fn = func(s string) string { 49 | return selectedItemStyle.Render("> " + s) 50 | } 51 | } 52 | 53 | fmt.Fprintf(w, fn(string(i))) 54 | } 55 | 56 | func NewApp() App { 57 | items := []list.Item{ 58 | Page("Popular"), 59 | Page("Recent"), 60 | Page("Rising"), 61 | Page("Oldest"), 62 | Page("Random"), 63 | Page("Submit"), 64 | } 65 | 66 | app := App{ 67 | list: list.New(items, itemDelegate{}, 0, 0), 68 | selected: false, 69 | isIdeaPage: false, 70 | ideaModel: models.NewIdeaModel(), 71 | submitModel: models.NewSubmitModel(), 72 | } 73 | 74 | app.list.Title = "What CLI" 75 | 76 | app.list.Styles.PaginationStyle = paginationStyle 77 | app.list.Styles.HelpStyle = helpStyle 78 | 79 | app.list.SetHeight(14) 80 | app.list.SetShowStatusBar(false) 81 | app.list.SetFilteringEnabled(false) 82 | app.list.SetShowFilter(false) 83 | 84 | return app 85 | } 86 | 87 | func (a App) Init() tea.Cmd { 88 | return nil 89 | } 90 | 91 | func (a App) getOrder() string { 92 | switch a.list.SelectedItem() { 93 | case Page("Popular"): 94 | return "POPULAR" 95 | case Page("Oldest"): 96 | return "OLDEST" 97 | case Page("Rising"): 98 | return "RISING" 99 | case Page("Recent"): 100 | return "RECENT" 101 | case Page("Random"): 102 | return "RANDOM" 103 | } 104 | 105 | return "" 106 | } 107 | 108 | func (a App) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 109 | 110 | switch msg := msg.(type) { 111 | case tea.KeyMsg: 112 | switch msg.String() { 113 | case "ctrl+c", "q", "esc": 114 | if a.selected { 115 | a.selected = false 116 | } else { 117 | return a, tea.Quit 118 | } 119 | case "enter", " ": 120 | switch a.list.SelectedItem() { 121 | case Page("Submit"): 122 | a.isIdeaPage = false 123 | a.selected = true 124 | default: 125 | if !a.selected { 126 | a.ideaModel.Clear() 127 | a.ideaModel.Order = a.getOrder() 128 | } 129 | 130 | a.selected = true 131 | a.isIdeaPage = true 132 | } 133 | } 134 | } 135 | 136 | var cmd tea.Cmd 137 | if a.selected { 138 | if a.isIdeaPage { 139 | a.ideaModel, _ = a.ideaModel.Update(msg) 140 | } else { 141 | a.submitModel, cmd = a.submitModel.Update(msg) 142 | } 143 | } else { 144 | a.list, _ = a.list.Update(msg) 145 | } 146 | 147 | return a, cmd 148 | } 149 | 150 | func (a App) View() string { 151 | v := a.list.View() 152 | 153 | if a.selected { 154 | if a.isIdeaPage { 155 | v = a.ideaModel.View() 156 | } else { 157 | v = a.submitModel.View() 158 | } 159 | } 160 | 161 | return v 162 | } 163 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/nore-dev/what-cli 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/charmbracelet/bubbles v0.10.2 7 | github.com/charmbracelet/bubbletea v0.19.3 8 | github.com/charmbracelet/lipgloss v0.4.0 9 | ) 10 | -------------------------------------------------------------------------------- /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.10.2 h1:VK1Q7nnBMDFTlrMmvBgE9nidtU5udsIcZvFXvjE2Cfk= 4 | github.com/charmbracelet/bubbles v0.10.2/go.mod h1:jOA+DUF1rjZm7gZHcNyIVW+YrBPALKfpGVdJu8UiJsA= 5 | github.com/charmbracelet/bubbletea v0.19.3 h1:OKeO/Y13rQQqt4snX+lePB0QrnW80UdrMNolnCcmoAw= 6 | github.com/charmbracelet/bubbletea v0.19.3/go.mod h1:VuXF2pToRxDUHcBUcPmCRUHRvFATM4Ckb/ql1rBl3KA= 7 | github.com/charmbracelet/harmonica v0.1.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= 8 | github.com/charmbracelet/lipgloss v0.4.0 h1:768h64EFkGUr8V5yAKV7/Ta0NiVceiPaV+PphaW1K9g= 9 | github.com/charmbracelet/lipgloss v0.4.0/go.mod h1:vmdkHvce7UzX6xkyf4cca8WlwdQ5RQr8fzta+xl7BOM= 10 | github.com/containerd/console v1.0.2 h1:Pi6D+aZXM+oUw1czuKgH5IJ+y0jhYcwBJfx5/Ghn9dE= 11 | github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ= 12 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 13 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 14 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 15 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 16 | github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA= 17 | github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 18 | github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= 19 | github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= 20 | github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= 21 | github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 22 | github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34= 23 | github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= 24 | github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ= 25 | github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= 26 | github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= 27 | github.com/muesli/termenv v0.9.0 h1:wnbOaGz+LUR3jNT0zOzinPnyDaCZUQRZj9GxK8eRVl8= 28 | github.com/muesli/termenv v0.9.0/go.mod h1:R/LzAKf+suGs4IsO95y7+7DpFHO0KABgnZqtlyx2mBw= 29 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 30 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 31 | github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 32 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= 33 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 34 | github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI= 35 | github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= 36 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 37 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 38 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 39 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk= 40 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 41 | golang.org/x/term v0.0.0-20210422114643-f5beecf764ed h1:Ei4bQjjpYUsS4efOUz+5Nz++IVkHk87n2zBA0NxBWc0= 42 | golang.org/x/term v0.0.0-20210422114643-f5beecf764ed/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 43 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | tea "github.com/charmbracelet/bubbletea" 7 | ) 8 | 9 | func main() { 10 | p := tea.NewProgram(NewApp(), tea.WithAltScreen()) 11 | 12 | if err := p.Start(); err != nil { 13 | os.Exit(1) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /models/idea.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | 9 | tea "github.com/charmbracelet/bubbletea" 10 | "github.com/charmbracelet/lipgloss" 11 | ) 12 | 13 | var ( 14 | ideaStyle = lipgloss.NewStyle().Border(lipgloss.RoundedBorder(), true).Padding(1).Width(40).Align(lipgloss.Center) 15 | titleStyle = lipgloss.NewStyle().Bold(true) 16 | descriptionStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#aaa")).Margin(1).Italic(true) 17 | likeStyle = lipgloss.NewStyle().UnsetAlign().Align(lipgloss.Left).Background(lipgloss.Color("#CD5C5C")). 18 | Foreground(lipgloss.Color("#fff")).PaddingLeft(1).PaddingRight(1) 19 | tagStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#0f0")) 20 | orderStyle = lipgloss.NewStyle(). 21 | Foreground(lipgloss.Color("#FFFDF5")). 22 | Background(lipgloss.Color("#25A065")). 23 | Padding(0, 1) 24 | helpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#626262")).MarginTop(2) 25 | modelStyle = lipgloss.NewStyle().MarginLeft(2) 26 | ) 27 | 28 | const API_URL = "https://what-to-code.com/api" 29 | 30 | type Tag struct { 31 | Value string `json:"value,omitempty"` 32 | } 33 | 34 | type Idea struct { 35 | Description string `json:"description,omitempty"` 36 | Id int `json:"id,omitempty"` 37 | Likes int `json:"likes,omitempty"` 38 | Title string `json:"title,omitempty"` 39 | Tags []Tag `json:"tags,omitempty"` 40 | } 41 | 42 | type IdeaModel struct { 43 | list []Idea 44 | index int 45 | page int 46 | Order string 47 | hasMore bool 48 | } 49 | 50 | func NewIdeaModel() IdeaModel { 51 | return IdeaModel{ 52 | index: 0, 53 | page: 0, 54 | list: []Idea{}, 55 | Order: "POPULAR", 56 | hasMore: true, 57 | } 58 | } 59 | 60 | func (i IdeaModel) Init() tea.Cmd { 61 | return nil 62 | } 63 | 64 | func (i IdeaModel) Update(msg tea.Msg) (IdeaModel, tea.Cmd) { 65 | 66 | // First Time 67 | if len(i.list) == 0 { 68 | i.getMoreIdeas() 69 | } 70 | 71 | switch msg := msg.(type) { 72 | case tea.KeyMsg: 73 | switch msg.String() { 74 | case "w", "up", "a", "k": 75 | if i.index > 0 { 76 | i.index -= 1 77 | } 78 | case "s", "down", "d", "j": 79 | if i.index < len(i.list) { 80 | i.index += 1 81 | } 82 | 83 | if i.index == len(i.list) { 84 | if !i.hasMore { 85 | i.index -= 1 86 | break 87 | } 88 | i.getMoreIdeas() 89 | } 90 | 91 | case " ", "enter": 92 | i.Like() 93 | 94 | } 95 | } 96 | 97 | return i, nil 98 | } 99 | 100 | func (i IdeaModel) renderTags() string { 101 | s := "" 102 | for _, tag := range i.currentIdea().Tags { 103 | s += "#" + tag.Value 104 | } 105 | 106 | return s 107 | } 108 | 109 | func (i *IdeaModel) getMoreIdeas() { 110 | 111 | if i.Order == "RANDOM" { 112 | i.getRandomIdea() 113 | return 114 | } 115 | 116 | var newIdeas []Idea 117 | 118 | res, _ := http.Get(i.getListUrl()) 119 | 120 | body, _ := ioutil.ReadAll(res.Body) 121 | err := json.Unmarshal(body, &newIdeas) 122 | 123 | if err != nil { 124 | fmt.Print(err) 125 | } 126 | 127 | defer res.Body.Close() 128 | 129 | if len(newIdeas) == 0 { 130 | i.index -= 1 131 | i.hasMore = false 132 | return 133 | } 134 | 135 | i.list = append(i.list, newIdeas...) 136 | i.page += 1 137 | } 138 | 139 | func (i *IdeaModel) getRandomIdea() { 140 | if len(i.list) == 0 { 141 | i.list = append(i.list, i.getIdea("random")) 142 | } else { 143 | *i.currentIdea() = i.getIdea("random") 144 | } 145 | } 146 | 147 | func (i IdeaModel) getListUrl() string { 148 | return fmt.Sprintf("%s/ideas?order=%s&page=%d", API_URL, i.Order, i.page) 149 | } 150 | 151 | func text_default(text string, def string) string { 152 | if len(text) == 0 { 153 | return "There is no " + def 154 | } 155 | return text 156 | } 157 | 158 | func (i *IdeaModel) Clear() { 159 | i.hasMore = true 160 | i.index = 0 161 | i.list = nil 162 | } 163 | 164 | func (i IdeaModel) Like() { 165 | http.Post(fmt.Sprintf("%s/ideas/%d/like", API_URL, i.currentIdea().Id), "", nil) 166 | 167 | *i.currentIdea() = i.getIdea(fmt.Sprintf("%d", i.currentIdea().Id)) 168 | } 169 | 170 | func (i IdeaModel) currentIdea() *Idea { 171 | if i.Order == "RANDOM" { 172 | return &i.list[0] 173 | } 174 | 175 | return &i.list[i.index] 176 | } 177 | 178 | func (i IdeaModel) getIdea(id string) Idea { 179 | var idea Idea 180 | 181 | res, _ := http.Get(fmt.Sprintf("%s/ideas/%s", API_URL, id)) 182 | 183 | body, _ := ioutil.ReadAll(res.Body) 184 | err := json.Unmarshal(body, &idea) 185 | 186 | if err != nil { 187 | fmt.Print(err) 188 | } 189 | 190 | defer res.Body.Close() 191 | 192 | return idea 193 | } 194 | 195 | func (i IdeaModel) View() string { 196 | s := titleStyle.Render(i.currentIdea().Title) + "\n" 197 | 198 | s += descriptionStyle.Render(text_default(i.currentIdea().Description, "description")) + "\n" 199 | s += likeStyle.Render(fmt.Sprintf("♥ %d", i.currentIdea().Likes)) + "\n\n" 200 | s += tagStyle.Render(text_default(i.renderTags(), "tag")) 201 | 202 | return modelStyle.Render(orderStyle.Render(i.Order) + "\n" + ideaStyle.Render(s) + 203 | helpStyle.Render("↑/k/w previous idea • ↓/j/s next idea • space like/dislike • q/esc quit")) 204 | } 205 | -------------------------------------------------------------------------------- /models/submit.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/charmbracelet/bubbles/textinput" 5 | tea "github.com/charmbracelet/bubbletea" 6 | "github.com/charmbracelet/lipgloss" 7 | ) 8 | 9 | var ( 10 | submitStyle = lipgloss.NewStyle().Border(lipgloss.RoundedBorder(), true).Width(35).Height(10).Padding(0, 1, 1) 11 | backgroundStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#FFFDF5")). 12 | Background(lipgloss.Color("#25A065")).Padding(0, 1) 13 | ) 14 | 15 | type SubmitModel struct { 16 | fields []textinput.Model 17 | index int 18 | } 19 | 20 | func NewSubmitModel() SubmitModel { 21 | title := textinput.New() 22 | title.Placeholder = "Title" 23 | title.CharLimit = 100 24 | title.Focus() 25 | 26 | description := textinput.New() 27 | description.Placeholder = "Description" 28 | description.CharLimit = 1000 29 | 30 | tags := textinput.New() 31 | tags.Placeholder = "Tags" 32 | 33 | return SubmitModel{ 34 | fields: []textinput.Model{ 35 | title, 36 | description, 37 | tags, 38 | }, 39 | index: 0, 40 | } 41 | } 42 | 43 | func (s SubmitModel) Init() tea.Cmd { 44 | return textinput.Blink 45 | } 46 | 47 | func (s *SubmitModel) Clear() { 48 | for i, _ := range s.fields { 49 | s.fields[i].SetValue("") 50 | } 51 | 52 | s.index = 0 53 | } 54 | 55 | func (s SubmitModel) Update(msg tea.Msg) (SubmitModel, tea.Cmd) { 56 | 57 | switch msg := msg.(type) { 58 | case tea.KeyMsg: 59 | switch msg.String() { 60 | case "up": 61 | if s.index > 0 { 62 | s.index -= 1 63 | s.fields[s.index].Focus() 64 | } 65 | case "down": 66 | if s.index < len(s.fields)-1 { 67 | s.index += 1 68 | s.fields[s.index].Focus() 69 | } 70 | } 71 | } 72 | 73 | s.fields[s.index], _ = s.fields[s.index].Update(msg) 74 | 75 | return s, nil 76 | } 77 | 78 | func (s SubmitModel) View() string { 79 | return backgroundStyle.Render("SUBMIT") + "\n" + 80 | submitStyle.Render(s.fields[s.index].View()) + 81 | helpStyle.Render("↑ previous field • ↓/j/s next field • enter submit • q/esc go back") 82 | } 83 | --------------------------------------------------------------------------------