charmbracelet / bubbletea

A powerful little TUI framework 🏗
MIT License
26.82k stars 773 forks source link

Multiple choices #27

Closed adrien-barret closed 3 years ago

adrien-barret commented 3 years ago

Hi,

I'm looking in the doc but don't fully understand how to create multiple model and view in the same file. like: create your own drink:

~ choose two flavors: [] orange [] strawberrie [] watermelon [] apple

~ select adds: [] candies [] mixed colors [] strange glass shape

curio77 commented 3 years ago

I think you don't need multiple models, just have two separate structs/maps/slices/whatever for the respective selection state (checked items, current item) and a separate field that holds which of the two lists is active, both in the same model. From this, you can render the two lists consecutively (or, if that's what you're after, both at once with the current item moving between lists).

adrien-barret commented 3 years ago

by chance do you get time to make a small example ? I tried that but it seems the view don't load and the keyboard movements get some issues as it go with two slices

curio77 commented 3 years ago

Here you go… This example shows both lists at once, does without any fancy colors and such, and can be quit via Esc or Ctrl-C. Items can be toggled via Space or Enter. As it is, it lacks a means of submitting the selection, does not validate your constraints (selecting two flavors), and can surely be improved/optimized, but you should get the idea.

package main

import (
    "fmt"

    tea "github.com/charmbracelet/bubbletea"
)

func main() {
    m := &model{
        flavors: []item{
            {"orange", false},
            {"strawberry", false},
            {"watermelon", false},
            {"apple", false},
        },
        adds: []item{
            {"candies", false},
            {"mixed colors", false},
            {"strange glass shape", false},
        },
    }
    if err := tea.NewProgram(m).Start(); err != nil {
        panic(fmt.Sprintf("failed to run program: %v", err))
    }
}

type model struct {
    flavors, adds []item
    list, item    int
}

type item struct {
    text    string
    checked bool
}

func (m *model) Init() tea.Cmd {
    return nil
}

func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch typed := msg.(type) {
    case tea.KeyMsg:
        return m, m.handleKeyMsg(typed)
    }
    return m, nil
}

func (m *model) handleKeyMsg(msg tea.KeyMsg) tea.Cmd {
    switch msg.String() {
    case "esc", "ctrl+c":
        return tea.Quit
    case " ", "enter":
        switch m.list {
        case 0:
            m.flavors[m.item].checked = !m.flavors[m.item].checked
        case 1:
            m.adds[m.item].checked = !m.adds[m.item].checked
        }
    case "up":
        if m.item > 0 {
            m.item--
        } else if m.list > 0 {
            m.list--
            m.item = len(m.flavors) - 1
        }
    case "down":
        switch m.list {
        case 0:
            if m.item+1 < len(m.flavors) {
                m.item++
            } else {
                m.list++
                m.item = 0
            }
        case 1:
            if m.item+1 < len(m.adds) {
                m.item++
            }
        }
    }
    return nil
}

func (m *model) View() string {
    curFlavor, curAdd := -1, -1
    switch m.list {
    case 0:
        curFlavor = m.item
    case 1:
        curAdd = m.item
    }
    return m.renderList("choose two flavors", m.flavors, curFlavor) +
        "\n" +
        m.renderList("select adds", m.adds, curAdd)
}

func (m *model) renderList(header string, items []item, selected int) string {
    out := "~ " + header + ":\n"
    for i, item := range items {
        sel := " "
        if i == selected {
            sel = ">"
        }
        check := " "
        if items[i].checked {
            check = "✓"
        }
        out += fmt.Sprintf("%s [%s] %s\n", sel, check, item.text)
    }
    return out
}
adrien-barret commented 3 years ago

thanks ! :)