Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add picker component #621

Open
wants to merge 30 commits into
base: master
Choose a base branch
from

Conversation

Broderick-Westrope
Copy link

@Broderick-Westrope Broderick-Westrope commented Sep 20, 2024

Resolves #9.

Adds a new "picker" component. By default this component takes an implementation of the State interface and displays it in a horizontal picker manner (as requested in the issue). Thorough unit tests are included :)

CleanShot 2024-09-20 at 17 45 10

Notes

  • Two implementations of the State interface are included:
    • ListState takes a generic slice/list to display.
    • IntState allows you to set a maximum and minimum integer, or decide to ignore max or min.
  • In addition to moving incrementally through the choices you can also enable "jumping" which will go to the first/last element (or max/min in the case of IntState).
  • "Cycling" is another option which allows you to go past the first/last item (or max/min) and it will loop back to the other end of the list.
    • For IntState you can enable this on a per-end basis. Meaning you can set a min of 0 and max of 5, and also ignore the max. Going above 5 will continue incrementing, but going below 0 will set the value to 5. This can of course be enabled or disable for both min and max.
  • "Stepping" is always enabled. This allows you to increment/decrement a certain number of times (10 by default) with one key press. This uses the "shift" modifier plus the next/previous key by default. To disable this feature you can set the step size to be 0 or unset the step key bindings. The step size is not validated, so a negative number may be used. It is up to each State implementation to validate each step (ie. beyond max/min) and to handle cycling, etc.
  • By default indicators will be displayed to show when the user can increment / decrement the value.
  • Methods are also included on the Model to get the individual, styled display components (indicators and the value). This allows the user to build a visually different component (such as a vertical picker).

Demo

The following can be put in a main package within the repo to test out the picker component:

package main

import (
	"github.com/charmbracelet/bubbles/picker"
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"
	"log"
)

func main() {
	m := Model{
		children: []Child{
			{
				label: "default IntState (-3 to 3)",
				model: picker.New(
					picker.NewIntState(-3, 3, 0, false, false),
				),
			},
			{
				label: "default IntState (-3 to infinity)",
				model: picker.New(
					picker.NewIntState(-3, 3, 0, false, true),
				),
			},
			{
				label: "IntState with cycles (-3 to infinity)",
				model: picker.New(
					picker.NewIntState(-3, 3, 0, false, true),
					picker.WithCycles(),
				),
			},
			{
				label: "IntState with stepping by 2 (-3 to infinity)",
				model: picker.New(
					picker.NewIntState(-3, 3, 0, false, true),
					picker.WithStepSize(2),
				),
			},
			{
				label: "default ListState (3 elements)",
				model: picker.New(
					picker.NewListState([]string{"One", "Two", "Three"}, 0),
				),
			},
			{
				label: "ListState without indicators (3 elements)",
				model: picker.New(
					picker.NewListState([]string{"One", "Two", "Three"}, 0),
					picker.WithoutIndicators(),
				),
			},
		},
	}

	if _, err := tea.NewProgram(m).Run(); err != nil {
		log.Fatal(err)
	}
}

type Model struct {
	cursor   int
	children []Child
}

type Child struct {
	label string
	model picker.Model
}

func (m Model) Init() tea.Cmd {
	for _, c := range m.children {
		c.model.Init()
	}
	return nil
}

func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.KeyMsg:
		switch msg.String() {
		case "ctrl+c", "q":
			return m, tea.Quit
		case "up", "k":
			if m.cursor > 0 {
				m.cursor--
			}
		case "down", "j":
			if m.cursor < len(m.children)-1 {
				m.cursor++
			}
		}
	}

	tempModel, cmd := m.children[m.cursor].model.Update(msg)
	m.children[m.cursor].model, _ = tempModel.(picker.Model)

	return m, cmd
}

func (m Model) View() string {
	var output string

	for i, c := range m.children {
		cursor := "  "
		if m.cursor == i {
			cursor = "> "
		}

		output += lipgloss.JoinHorizontal(lipgloss.Center,
			cursor,
			c.label,
			":\t",
			c.model.View(),
			"\n",
		)
	}

	return output + "\n"
}

@meowgorithm
Copy link
Member

meowgorithm commented Sep 20, 2024

This’ll be a fun one. A few quick thoughts on very first read:

  • NewModel should be New
  • For numerics, the shift modifier should allow you to increment by increment * 10

@Broderick-Westrope
Copy link
Author

Broderick-Westrope commented Sep 21, 2024

@meowgorithm thanks for the quick feedback!

These commits fix the New method and add "stepping" (incrementing by a set count using the shift modifier; uses 10 by default). Keen to hear more thoughts when you have a chance :)

P.S. Flexible on the naming of stepping, jumping, etc. I just went with the first things that came to mind. Perhaps it would be better as:

  • Next() -> Increment()
  • StepForward(size int) -> IncrementBy(count int)
  • JumpForward() -> JumpToEnd() / GoToEnd()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add plus/minus button widget please
2 participants