github.com/wolfi-dev/wolfictl@v0.16.11/pkg/cli/components/picker/picker.go (about)

     1  package picker
     2  
     3  import (
     4  	"fmt"
     5  	"strings"
     6  
     7  	tea "github.com/charmbracelet/bubbletea"
     8  	"github.com/wolfi-dev/wolfictl/pkg/cli/components/breather"
     9  	"github.com/wolfi-dev/wolfictl/pkg/cli/components/ctrlcwrapper"
    10  	"github.com/wolfi-dev/wolfictl/pkg/cli/styles"
    11  )
    12  
    13  type Model[T any] struct {
    14  	items               []T
    15  	itemRendererFunc    func(T) string
    16  	customActions       []CustomAction[T]
    17  	selected            int
    18  	picked              bool
    19  	aboutToExit         bool
    20  	breather            breather.Model
    21  	messageForZeroItems string
    22  
    23  	// Error is the error that occurred during the picker's lifecycle, if any.
    24  	Error error
    25  }
    26  
    27  type Options[T any] struct {
    28  	// Items is the list of items to display, from which the user can pick.
    29  	Items []T
    30  
    31  	// MessageForZeroItems is the message to display when there are no items to
    32  	// list.
    33  	MessageForZeroItems string
    34  
    35  	// ItemRenderFunc is the function to use to render each item in the list. If
    36  	// nil, the item will be rendered via fmt.Sprintf("%v", item).
    37  	ItemRenderFunc func(T) string
    38  
    39  	// CustomActions is a list of custom actions that the user can take.
    40  	CustomActions []CustomAction[T]
    41  }
    42  
    43  // New returns a new picker model. The render function is used to render each
    44  // item in the list.
    45  func New[T any](opts Options[T]) Model[T] {
    46  	return Model[T]{
    47  		items:               opts.Items,
    48  		itemRendererFunc:    opts.ItemRenderFunc,
    49  		customActions:       opts.CustomActions,
    50  		messageForZeroItems: opts.MessageForZeroItems,
    51  		breather:            breather.New(">"),
    52  	}
    53  }
    54  
    55  type CustomAction[T any] struct {
    56  	// Key is the key that the user should press to select this action.
    57  	Key string
    58  
    59  	// Description is a short description of what this action does, used in help
    60  	// text.
    61  	//
    62  	// For example, in the help text "o to open.", "to open" is the description.
    63  	Description string
    64  
    65  	// Do is the function to call when the user presses the key for this action.
    66  	Do func(selected T) tea.Cmd
    67  }
    68  
    69  // ErrCmd returns a command that will cause the picker to display an error
    70  // message and exit.
    71  func ErrCmd(err error) tea.Cmd {
    72  	return func() tea.Msg {
    73  		return ErrMsg(err)
    74  	}
    75  }
    76  
    77  // ErrMsg is a tea.Msg that indicates an error occurred during the picker's
    78  // lifecycle.
    79  type ErrMsg error
    80  
    81  func (m Model[T]) Init() tea.Cmd {
    82  	return m.breather.Init()
    83  }
    84  
    85  func (m Model[T]) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    86  	switch msg := msg.(type) {
    87  	case ctrlcwrapper.AboutToExitMsg:
    88  		m.aboutToExit = true
    89  		return m, ctrlcwrapper.InnerIsReady
    90  
    91  	case ErrMsg:
    92  		m.Error = msg
    93  		return m, tea.Quit
    94  
    95  	case tea.KeyMsg:
    96  		switch msg.String() {
    97  		case "up":
    98  			if m.selected > 0 {
    99  				m.selected--
   100  			}
   101  
   102  		case "down":
   103  			if m.selected < len(m.items)-1 {
   104  				m.selected++
   105  			}
   106  
   107  		case "enter":
   108  			m.picked = true
   109  			m.aboutToExit = true
   110  			return m, tea.Quit
   111  		}
   112  
   113  		for _, action := range m.customActions {
   114  			if msg.String() == action.Key {
   115  				if action.Do == nil {
   116  					continue
   117  				}
   118  
   119  				var selected T
   120  				if len(m.items) > 0 {
   121  					selected = m.items[m.selected]
   122  				}
   123  				return m, action.Do(selected)
   124  			}
   125  		}
   126  
   127  	case breather.TickMsg:
   128  		var cmd tea.Cmd
   129  		m.breather, cmd = m.breather.Update(msg)
   130  		return m, cmd
   131  	}
   132  
   133  	return m, nil
   134  }
   135  
   136  func (m Model[T]) View() string {
   137  	sb := new(strings.Builder)
   138  
   139  	if len(m.items) == 0 {
   140  		sb.WriteString(m.messageForZeroItems + "\n")
   141  	}
   142  
   143  	var breatherView string
   144  	if m.aboutToExit {
   145  		// Don't animate, picking and/or exiting has happened.
   146  		breatherView = m.breather.ViewStatic()
   147  	} else {
   148  		breatherView = m.breather.View()
   149  	}
   150  
   151  	for i, item := range m.items {
   152  		if i == m.selected {
   153  			sb.WriteString(fmt.Sprintf("%s %s\n", breatherView, m.itemRendererFunc(item)))
   154  			continue
   155  		}
   156  
   157  		if !m.aboutToExit {
   158  			sb.WriteString(fmt.Sprintf("  %s\n", m.itemRendererFunc(item)))
   159  		}
   160  	}
   161  
   162  	sb.WriteString("\n")
   163  
   164  	if !m.aboutToExit {
   165  		if count := len(m.items); count > 0 {
   166  			if count > 1 {
   167  				sb.WriteString(helpChangeSelection + " ")
   168  			}
   169  
   170  			sb.WriteString(helpConfirm + "\n")
   171  		}
   172  
   173  		for _, action := range m.customActions {
   174  			sb.WriteString(fmt.Sprintf(
   175  				"%s %s\n",
   176  				styleHelpKey.Render(action.Key),
   177  				styleHelpExplanation.Render(action.Description+"."),
   178  			))
   179  		}
   180  	}
   181  
   182  	return sb.String()
   183  }
   184  
   185  // Picked returns the item that was picked by the user, if any. If no item has
   186  // been picked, this returns nil.
   187  func (m Model[T]) Picked() *T {
   188  	if !m.picked {
   189  		// The user hasn't picked anything yet.
   190  		return nil
   191  	}
   192  
   193  	picked := m.items[m.selected]
   194  	return &picked
   195  }
   196  
   197  var (
   198  	helpChangeSelection = fmt.Sprintf(
   199  		"%s %s",
   200  		styleHelpKey.Render("↑/↓"),
   201  		styleHelpExplanation.Render("to change selection."),
   202  	)
   203  
   204  	helpConfirm = fmt.Sprintf(
   205  		"%s %s",
   206  		styleHelpKey.Render("enter"),
   207  		styleHelpExplanation.Render("to confirm."),
   208  	)
   209  )
   210  
   211  var (
   212  	styleHelpKey         = styles.FaintAccent().Copy().Bold(true)
   213  	styleHelpExplanation = styles.Faint().Copy()
   214  )