Skip to content

Commit

Permalink
feat: better loading
Browse files Browse the repository at this point in the history
  • Loading branch information
maaslalani committed May 9, 2024
1 parent cd622ff commit 81c646c
Show file tree
Hide file tree
Showing 13 changed files with 60 additions and 38 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,6 @@

# Go workspace file
go.work

# Debugging
debug.log
11 changes: 9 additions & 2 deletions eval.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package huh

import "github.com/mitchellh/hashstructure/v2"
import (
"time"

"github.com/mitchellh/hashstructure/v2"
)

type Eval[T any] struct {

Check warning on line 9 in eval.go

View workflow job for this annotation

GitHub Actions / lint

exported: exported type Eval should have comment or be unexported (revive)
val T
Expand All @@ -10,9 +14,12 @@ type Eval[T any] struct {
bindingsHash uint64
cache map[uint64]T

loading bool
loading bool
loadingStart time.Time
}

const spinnerShowThreshold = 25 * time.Millisecond

func hash(val any) uint64 {
hash, _ := hashstructure.Hash(val, hashstructure.FormatV2, nil)
return hash
Expand Down
3 changes: 2 additions & 1 deletion examples/dynamic/dynamic-country/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"fmt"
"strings"
"time"

"github.com/charmbracelet/log"
Expand Down Expand Up @@ -48,7 +49,7 @@ func main() {
return fmt.Sprintf("You selected: %s", country)
}, &country).
DescriptionFunc(func() string {
return fmt.Sprintf("You selected: %s", state)
return fmt.Sprintf("You selected: %s", strings.Join(state, ", "))
}, []any{&country, &state}),
),
)
Expand Down
4 changes: 0 additions & 4 deletions field_confirm.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,6 @@ func (c *Confirm) Title(title string) *Confirm {

// TitleFunc sets the title func of the confirm field.
func (c *Confirm) TitleFunc(f func() string, bindings any) *Confirm {
c.title.val = "Loading..."
c.title.fn = f
c.title.bindings = bindings
return c
Expand All @@ -121,7 +120,6 @@ func (c *Confirm) Description(description string) *Confirm {

// DescriptionFunc sets the description function of the confirm field.
func (c *Confirm) DescriptionFunc(f func() string, bindings any) *Confirm {
c.description.val = "Loading..."
c.description.fn = f
c.description.bindings = bindings
return c
Expand Down Expand Up @@ -165,7 +163,6 @@ func (c *Confirm) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if ok, hash := c.title.shouldUpdate(); ok {
c.title.bindingsHash = hash
if !c.title.loadFromCache() {
c.title.val = "Loading..."
c.title.loading = true
cmds = append(cmds, func() tea.Msg {
return updateTitleMsg{id: c.id, title: c.title.fn(), hash: hash}
Expand All @@ -175,7 +172,6 @@ func (c *Confirm) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if ok, hash := c.description.shouldUpdate(); ok {
c.description.bindingsHash = hash
if !c.description.loadFromCache() {
c.description.val = "Loading..."
c.description.loading = true
cmds = append(cmds, func() tea.Msg {
return updateDescriptionMsg{id: c.id, description: c.description.fn(), hash: hash}
Expand Down
5 changes: 0 additions & 5 deletions field_input.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,15 +92,13 @@ func (i *Input) Description(description string) *Input {
func (i *Input) TitleFunc(f func() string, bindings any) *Input {
i.title.fn = f
i.title.bindings = bindings
i.title.val = "Loading..."
return i
}

// DescriptionFunc sets the description func of the text field.
func (i *Input) DescriptionFunc(f func() string, bindings any) *Input {
i.description.fn = f
i.description.bindings = bindings
i.description.val = "Loading..."
return i
}

Expand Down Expand Up @@ -258,7 +256,6 @@ func (i *Input) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if ok, hash := i.title.shouldUpdate(); ok {
i.title.bindingsHash = hash
if !i.title.loadFromCache() {
i.title.val = "Loading..."
i.title.loading = true
cmds = append(cmds, func() tea.Msg {
return updateTitleMsg{id: i.id, title: i.title.fn(), hash: hash}
Expand All @@ -268,7 +265,6 @@ func (i *Input) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if ok, hash := i.description.shouldUpdate(); ok {
i.description.bindingsHash = hash
if !i.description.loadFromCache() {
i.description.val = "Loading..."
i.description.loading = true
cmds = append(cmds, func() tea.Msg {
return updateDescriptionMsg{id: i.id, description: i.description.fn(), hash: hash}
Expand All @@ -280,7 +276,6 @@ func (i *Input) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if i.placeholder.loadFromCache() {
i.textinput.Placeholder = i.placeholder.val
} else {
i.placeholder.val = "Loading..."
i.placeholder.loading = true
cmds = append(cmds, func() tea.Msg {
return updatePlaceholderMsg{id: i.id, placeholder: i.placeholder.fn(), hash: hash}
Expand Down
31 changes: 23 additions & 8 deletions field_multiselect.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ package huh
import (
"fmt"
"strings"
"time"

"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/spinner"
"github.com/charmbracelet/bubbles/textinput"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
Expand Down Expand Up @@ -37,6 +39,7 @@ type MultiSelect[T comparable] struct {
filtering bool
filter textinput.Model
viewport viewport.Model
spinner spinner.Model

// options
width int
Expand All @@ -50,6 +53,8 @@ func NewMultiSelect[T comparable]() *MultiSelect[T] {
filter := textinput.New()
filter.Prompt = "/"

s := spinner.New(spinner.WithSpinner(spinner.Line))

return &MultiSelect[T]{
id: nextID(),
options: Eval[[]Option[T]]{cache: make(map[uint64][]Option[T])},
Expand All @@ -59,6 +64,7 @@ func NewMultiSelect[T comparable]() *MultiSelect[T] {
validate: func([]T) error { return nil },
filtering: false,
filter: filter,
spinner: s,
}
}

Expand Down Expand Up @@ -94,7 +100,6 @@ func (m *MultiSelect[T]) Title(title string) *MultiSelect[T] {
func (m *MultiSelect[T]) TitleFunc(f func() string, bindings any) *MultiSelect[T] {
m.title.fn = f
m.title.bindings = bindings
m.title.val = "Loading..."
return m
}

Expand All @@ -108,7 +113,6 @@ func (m *MultiSelect[T]) Description(description string) *MultiSelect[T] {
func (m *MultiSelect[T]) DescriptionFunc(f func() string, bindings any) *MultiSelect[T] {
m.description.fn = f
m.description.bindings = bindings
m.description.val = "Loading..."
return m
}

Expand Down Expand Up @@ -223,13 +227,16 @@ func (m *MultiSelect[T]) Init() tea.Cmd {

// Update updates the multi-select field.
func (m *MultiSelect[T]) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd

// Enforce height on the viewport during update as we need themes to
// be applied before we can calculate the height.
m.updateViewportHeight()

var cmd tea.Cmd
if m.filtering {
m.filter, cmd = m.filter.Update(msg)
cmds = append(cmds, cmd)
}

switch msg := msg.(type) {
Expand All @@ -238,7 +245,6 @@ func (m *MultiSelect[T]) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if ok, hash := m.title.shouldUpdate(); ok {
m.title.bindingsHash = hash
if !m.title.loadFromCache() {
m.title.val = "Loading..."
m.title.loading = true
cmds = append(cmds, func() tea.Msg {
return updateTitleMsg{id: m.id, title: m.title.fn(), hash: hash}
Expand All @@ -248,7 +254,6 @@ func (m *MultiSelect[T]) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if ok, hash := m.description.shouldUpdate(); ok {
m.description.bindingsHash = hash
if !m.description.loadFromCache() {
m.description.val = "Loading..."
m.description.loading = true
cmds = append(cmds, func() tea.Msg {
return updateDescriptionMsg{id: m.id, description: m.description.fn(), hash: hash}
Expand All @@ -263,14 +268,22 @@ func (m *MultiSelect[T]) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.cursor = clamp(m.cursor, 0, len(m.filteredOptions)-1)
} else {
m.options.loading = true
m.options.loadingStart = time.Now()
cmds = append(cmds, func() tea.Msg {
return updateOptionsMsg[T]{id: m.id, options: m.options.fn(), hash: hash}
})
}, m.spinner.Tick)
}
}

return m, tea.Batch(cmds...)

case spinner.TickMsg:
if !m.options.loading {
break
}
m.spinner, cmd = m.spinner.Update(msg)
return m, cmd

case updateTitleMsg:
if msg.id == m.id && msg.hash == m.title.bindingsHash {
m.title.update(msg.title)
Expand Down Expand Up @@ -384,7 +397,7 @@ func (m *MultiSelect[T]) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
}

return m, cmd
return m, tea.Batch(cmds...)
}

// updateViewportHeight updates the viewport size according to the Height setting
Expand Down Expand Up @@ -464,8 +477,9 @@ func (m *MultiSelect[T]) optionsView() string {
sb strings.Builder
)

if m.options.loading {
sb.WriteString(c + styles.SelectedOption.Render("Loading..."))
if m.options.loading && time.Since(m.options.loadingStart) > spinnerShowThreshold {
m.spinner.Style = m.activeStyles().MultiSelectSelector.UnsetString()
sb.WriteString(m.spinner.View() + " Loading...")
return sb.String()
}

Expand Down Expand Up @@ -498,6 +512,7 @@ func (m *MultiSelect[T]) optionsView() string {
// View renders the multi-select field.
func (m *MultiSelect[T]) View() string {
styles := m.activeStyles()

m.viewport.SetContent(m.optionsView())

var sb strings.Builder
Expand Down
3 changes: 0 additions & 3 deletions field_note.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ func (n *Note) Title(title string) *Note {
func (n *Note) TitleFunc(f func() string, bindings any) *Note {
n.title.fn = f
n.title.bindings = bindings
n.title.val = "Loading..."
return n
}

Expand All @@ -66,7 +65,6 @@ func (n *Note) Description(description string) *Note {
func (n *Note) DescriptionFunc(f func() string, bindings any) *Note {
n.description.fn = f
n.description.bindings = bindings
n.description.val = "Loading..."
return n
}

Expand Down Expand Up @@ -127,7 +125,6 @@ func (n *Note) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if ok, hash := n.title.shouldUpdate(); ok {
n.title.bindingsHash = hash
if !n.title.loadFromCache() {
n.title.val = "Loading..."
n.title.loading = true
cmds = append(cmds, func() tea.Msg {
return updateTitleMsg{id: n.id, title: n.title.fn(), hash: hash}
Expand Down
11 changes: 0 additions & 11 deletions field_select.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,6 @@ func (s *Select[T]) Title(title string) *Select[T] {
func (s *Select[T]) TitleFunc(f func() string, bindings any) *Select[T] {
s.title.fn = f
s.title.bindings = bindings
s.title.val = "Loading..."
return s
}

Expand All @@ -130,7 +129,6 @@ func (s *Select[T]) Description(description string) *Select[T] {
func (s *Select[T]) DescriptionFunc(f func() string, bindings any) *Select[T] {
s.description.fn = f
s.description.bindings = bindings
s.description.val = "Loading..."
return s
}

Expand Down Expand Up @@ -271,7 +269,6 @@ func (s *Select[T]) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if ok, hash := s.title.shouldUpdate(); ok {
s.title.bindingsHash = hash
if !s.title.loadFromCache() {
s.title.val = "Loading..."
s.title.loading = true
cmds = append(cmds, func() tea.Msg {
return updateTitleMsg{id: s.id, title: s.title.fn(), hash: hash}
Expand Down Expand Up @@ -474,9 +471,6 @@ func (s *Select[T]) titleView() string {
}

func (s *Select[T]) descriptionView() string {
if s.description.loading {
return s.activeStyles().Description.Render("Loading...")
}
return s.activeStyles().Description.Render(s.description.val)
}

Expand All @@ -487,11 +481,6 @@ func (s *Select[T]) optionsView() string {
sb strings.Builder
)

if s.options.loading {
sb.WriteString(c + styles.SelectedOption.Render("Loading..."))
return sb.String()
}

if s.inline {
sb.WriteString(styles.PrevIndicator.Faint(s.selected <= 0).String())
if len(s.filteredOptions) > 0 {
Expand Down
3 changes: 0 additions & 3 deletions field_text.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,15 +102,13 @@ func (t *Text) Description(description string) *Text {
func (t *Text) TitleFunc(f func() string, bindings any) *Text {
t.title.fn = f
t.title.bindings = bindings
t.title.val = "Loading..."
return t
}

// Description sets the description of the text field.
func (t *Text) DescriptionFunc(f func() string, bindings any) *Text {
t.description.fn = f
t.description.bindings = bindings
t.description.val = "Loading..."
return t
}

Expand Down Expand Up @@ -264,7 +262,6 @@ func (t *Text) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if ok, hash := t.description.shouldUpdate(); ok {
t.description.bindingsHash = hash
if !t.description.loadFromCache() {
t.description.val = "Loading..."
t.description.loading = true
cmds = append(cmds, func() tea.Msg {
return updateDescriptionMsg{id: t.id, description: t.description.fn(), hash: hash}
Expand Down
7 changes: 7 additions & 0 deletions form.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/paginator"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/log"
)

const defaultWidth = 80
Expand Down Expand Up @@ -551,6 +552,12 @@ func (f *Form) View() string {

// Run runs the form.
func (f *Form) Run() error {
debugFile, err := tea.LogToFile("debug.log", "debug")
if err != nil {
return err

Check failure on line 557 in form.go

View workflow job for this annotation

GitHub Actions / lint-soft

error returned from external package is unwrapped: sig: func github.com/charmbracelet/bubbletea.LogToFile(path string, prefix string) (*os.File, error) (wrapcheck)
}
log.SetOutput(debugFile)

f.submitCmd = tea.Quit
f.cancelCmd = tea.Quit

Expand Down
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ require (
github.com/charmbracelet/bubbles v0.18.0
github.com/charmbracelet/bubbletea v0.26.1
github.com/charmbracelet/lipgloss v0.10.1-0.20240506202754-3ee5dcab73cb
github.com/charmbracelet/log v0.4.0
github.com/charmbracelet/x/exp/strings v0.0.0-20240506152644-8135bef4e495
github.com/charmbracelet/x/exp/term v0.0.0-20240506152644-8135bef4e495
github.com/mitchellh/hashstructure/v2 v2.0.2
Expand All @@ -17,6 +18,7 @@ require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
Expand All @@ -26,6 +28,7 @@ require (
github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.15.2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/term v0.20.0 // indirect
Expand Down
Loading

0 comments on commit 81c646c

Please sign in to comment.