charmbracelet / huh

Build terminal forms and prompts 🤷🏻‍♀️
MIT License
4.23k stars 115 forks source link

feat: Set max height on a Select component #113

Closed marwanhawari closed 7 months ago

marwanhawari commented 8 months ago

Is your feature request related to a problem? Please describe. I have a select component where the number of input elements is dynamic. It can be as few as 2 or 3 elements or even a few dozen. The problem is that if I don't set a height on a select component and I have lots of elements to display, the selection starts in the middle of the list.

Screenshot 2024-01-04 at 1 06 16 PM

If I set a fixed height, this fixes my problem for big lists:

Screenshot 2024-01-04 at 1 05 29 PM

However, for short lists, I get a UI that is too big:

Screenshot 2024-01-04 at 1 05 09 PM

Describe the solution you'd like My ideal UI would be what survey does by default:

Screenshot 2024-01-04 at 1 06 25 PM Screenshot 2024-01-04 at 1 06 40 PM

For huh I would maybe imagine a .MaxHeight() method so that the small list would still fit the contents, but the big list would be contained within the MaxHeight value.

// Select is a form select field.
type Select[T comparable] struct {
    value    *T
    key      string
    viewport viewport.Model

    // customization
    title           string
    description     string
    options         []Option[T]
    filteredOptions []Option[T]
    height          int
+   maxHeight       int

...

+ func (s *Select[T]) MaxHeight(height int) *Select[T] {
+   s.maxHeight = height
+   s.updateViewportHeight()
+   return s
+ }

func (s *Select[T]) updateViewportHeight() {
    // If no height is set size the viewport to the number of options.
-   if s.height <= 0 {
-       s.viewport.Height = len(s.options)
-       return
-   }
+   if s.height <= 0 {
+       s.viewport.Height = min(len(s.options), s.maxHeight)
+       return
+   }

    // Wait until the theme has appied.
    if s.theme == nil {
        return
    }

    const minHeight = 1
-   s.viewport.Height = max(minHeight, s.height-
-       lipgloss.Height(s.titleView())-
-       lipgloss.Height(s.descriptionView()))
+   s.viewport.Height = max(minHeight, min(s.height-
+       lipgloss.Height(s.titleView())-
+       lipgloss.Height(s.descriptionView()), s.maxHeight))
}

This would result in the desired UI state:

Screenshot 2024-01-04 at 1 05 29 PM Screenshot 2024-01-04 at 1 06 01 PM

Describe alternatives you've considered Have the .Height() method shrink to fit the contents by default and add a parameter to .Height() to allow users to make it a fixed height if they want.

Additional context Here's how I'm currently using the Select component:

// PromptSelect launches the selection UI
func PromptSelect(message string, options []string) (string, error) {
    var result string
    form := huh.NewForm(
        huh.NewGroup(
            huh.NewSelect[string]().
                Title(message).
                Height(10).
                Options(huh.NewOptions(options...)...).
                Value(&result),
        ),
    ).WithKeyMap(NewKeyMap())

    err := form.Run()
    if err != nil {
        return "", ExitUserSelectionError{Err: err}
    }
    return result, nil
}
maaslalani commented 7 months ago

Hey there! Thank you so much for creating this issue.

We introduced the Height attribute on the Select and MultiSelect fields to give them the ability to scroll. Let us know if that fixes your issue! If it doesn't work for you, please feel free to reopen this issue!