Skip to content

Commit

Permalink
Bump Bubbles to v0.14.0, update accordingly, and simplify spinners
Browse files Browse the repository at this point in the history
* Re-implement spinner min/max lifetimes in pager
* Remove generalized spin commands in favor of model-level commands
* Update textinput, viewport, and spinner constructors
  • Loading branch information
meowgorithm authored and muesli committed Nov 11, 2022
1 parent 9c9b3e6 commit db7f49b
Show file tree
Hide file tree
Showing 5 changed files with 70 additions and 55 deletions.
2 changes: 1 addition & 1 deletion ui/config.go
Expand Up @@ -7,7 +7,7 @@ type Config struct {
HomeDir string `env:"HOME"`
GlamourMaxWidth uint
GlamourStyle string
EnableMouse bool
EnableMouse bool

// Which directory should we start from?
WorkingDirectory string
Expand Down
53 changes: 34 additions & 19 deletions ui/pager.go
Expand Up @@ -117,7 +117,9 @@ type pagerModel struct {
state pagerState
showHelp bool
textInput textinput.Model
spinner spinner.Model

spinner spinner.Model
spinnerStart time.Time

statusMessage string
statusMessageTimer *time.Timer
Expand All @@ -133,12 +135,12 @@ type pagerModel struct {

func newPagerModel(common *commonModel) pagerModel {
// Init viewport
vp := viewport.Model{}
vp := viewport.New(0, 0)
vp.YPosition = 0
vp.HighPerformanceRendering = config.HighPerformancePager

// Text input for notes/memos
ti := textinput.NewModel()
ti := textinput.New()
ti.Prompt = " > "
ti.PromptStyle = pagerNoteInputPromptStyle
ti.TextStyle = pagerNoteInputStyle
Expand All @@ -147,10 +149,8 @@ func newPagerModel(common *commonModel) pagerModel {
ti.Focus()

// Text input for search
sp := spinner.NewModel()
sp := spinner.New()
sp.Style = spinnerStyle
sp.HideFor = time.Millisecond * 50
sp.MinimumLifetime = time.Millisecond * 180

return pagerModel{
common: common,
Expand Down Expand Up @@ -297,11 +297,11 @@ func (m pagerModel) update(msg tea.Msg) (pagerModel, tea.Cmd) {
// Stash a local document
if m.state != pagerStateStashing && stashableDocTypes.Contains(md.docType) {
m.state = pagerStateStashing
m.spinner.Start()
m.spinnerStart = time.Now()
cmds = append(
cmds,
stashDocument(m.common.cc, md),
spinner.Tick,
m.spinner.Tick,
)
}
case "?":
Expand All @@ -313,15 +313,19 @@ func (m pagerModel) update(msg tea.Msg) (pagerModel, tea.Cmd) {
}

case spinner.TickMsg:
if m.state == pagerStateStashing || m.spinner.Visible() {
// If we're still stashing, or if the spinner still needs to
// finish, spin it along.
newSpinnerModel, cmd := m.spinner.Update(msg)
m.spinner = newSpinnerModel
spinnerMinTimeout := m.spinnerStart.
Add(spinnerVisibilityTimeout).
Add(spinnerMinLifetime)

if m.state == pagerStateStashing || time.Now().Before(spinnerMinTimeout) {
// We're either still stashing or we haven't reached the spinner's
// full lifetime. In either case we need to spin the spinner
// irrespective of it's more fine-grained visibility rules.
var cmd tea.Cmd
m.spinner, cmd = m.spinner.Update(msg)
cmds = append(cmds, cmd)
} else if m.state == pagerStateStashSuccess && !m.spinner.Visible() {
// If the spinner's finished and we haven't told the user the
// stash was successful, do that.
} else if m.state == pagerStateStashSuccess {
// Successful stash. Stop spinning and update accordingly.
m.state = pagerStateBrowse
m.currentDocument = *m.stashedDocument
m.stashedDocument = nil
Expand All @@ -346,7 +350,8 @@ func (m pagerModel) update(msg tea.Msg) (pagerModel, tea.Cmd) {
// message in the main update function where we're adding this stashed
// item to the stash listing.
m.state = pagerStateStashSuccess
if !m.spinner.Visible() {

if !m.spinnerVisible() {
// The spinner has finished spinning, so tell the user the stash
// was successful.
m.state = pagerStateBrowse
Expand Down Expand Up @@ -378,6 +383,14 @@ func (m pagerModel) update(msg tea.Msg) (pagerModel, tea.Cmd) {
return m, tea.Batch(cmds...)
}

// spinnerVisible returns whether or not the spinner should be drawn.
func (m pagerModel) spinnerVisible() bool {
windowStart := m.spinnerStart.Add(spinnerVisibilityTimeout)
windowEnd := windowStart.Add(spinnerMinLifetime)
now := time.Now()
return now.After(windowStart) && now.Before(windowEnd)
}

func (m pagerModel) View() string {
var b strings.Builder
fmt.Fprint(&b, m.viewport.View()+"\n")
Expand Down Expand Up @@ -431,9 +444,11 @@ func (m pagerModel) statusBarView(b *strings.Builder) {
// Status indicator; spinner or stash dot
var statusIndicator string
if m.state == pagerStateStashing || m.state == pagerStateStashSuccess {
if m.spinner.Visible() {
statusIndicator = statusBarNoteStyle(" ") + m.spinner.View()
var spinner string
if m.spinnerVisible() {
spinner = m.spinner.View()
}
statusIndicator = statusBarNoteStyle(" ") + spinner
} else if isStashed && showStatusMessage {
statusIndicator = statusBarMessageStashIconStyle(" " + pagerStashIcon)
} else if isStashed {
Expand Down
53 changes: 24 additions & 29 deletions ui/stash.go
Expand Up @@ -29,7 +29,7 @@ const (
)

var (
stashedStatusMessage = statusMessage{normalStatusMessage, "Stashed!"}
stashingStatusMessage = statusMessage{normalStatusMessage, "Stashing..."}
alreadyStashedStatusMessage = statusMessage{subtleStatusMessage, "Already stashed"}
)

Expand Down Expand Up @@ -253,6 +253,14 @@ func (m stashModel) online() bool {
return !m.localOnly() && m.common.authStatus == authOK
}

// Whether or not the spinner should be spinning.
func (m stashModel) shouldSpin() bool {
loading := !m.loadingDone()
stashing := m.common.isStashing()
openingDocument := m.viewState == stashStateLoadingDocument
return loading || stashing || openingDocument
}

func (m *stashModel) setSize(width, height int) {
m.common.width = width
m.common.height = height
Expand Down Expand Up @@ -444,7 +452,7 @@ func (m *stashModel) openMarkdown(md *markdown) tea.Cmd {
cmd = loadRemoteMarkdown(m.common.cc, md)
}

return tea.Batch(cmd, spinner.Tick)
return tea.Batch(cmd, m.spinner.Tick)
}

func (m *stashModel) newStatusMessage(sm statusMessage) tea.Cmd {
Expand Down Expand Up @@ -509,21 +517,18 @@ func (m *stashModel) moveCursorDown() {
// INIT

func newStashModel(common *commonModel) stashModel {
sp := spinner.NewModel()
sp := spinner.New()
sp.Spinner = spinner.Line
sp.Style = stashSpinnerStyle
sp.HideFor = time.Millisecond * 100
sp.MinimumLifetime = time.Millisecond * 180
sp.Start()

ni := textinput.NewModel()
ni := textinput.New()
ni.Prompt = "Memo:"
ni.PromptStyle = stashInputPromptStyle
ni.CursorStyle = stashInputCursorStyle
ni.CharLimit = noteCharacterLimit
ni.Focus()

si := textinput.NewModel()
si := textinput.New()
si.Prompt = "Find:"
si.PromptStyle = stashInputPromptStyle
si.CursorStyle = stashInputCursorStyle
Expand Down Expand Up @@ -647,14 +652,9 @@ func (m stashModel) update(msg tea.Msg) (stashModel, tea.Cmd) {
return m, nil

case spinner.TickMsg:
loading := !m.loadingDone()
stashing := m.common.isStashing()
openingDocument := m.viewState == stashStateLoadingDocument
spinnerVisible := m.spinner.Visible()

if loading || stashing || openingDocument || spinnerVisible {
newSpinnerModel, cmd := m.spinner.Update(msg)
m.spinner = newSpinnerModel
if m.shouldSpin() {
var cmd tea.Cmd
m.spinner, cmd = m.spinner.Update(msg)
cmds = append(cmds, cmd)
}

Expand All @@ -667,10 +667,9 @@ func (m stashModel) update(msg tea.Msg) (stashModel, tea.Cmd) {
}
}

// Note: mechanical stuff related to stash success is handled in the parent
// update function.
case stashSuccessMsg:
m.spinner.Finish()
// No-op: mechanical stuff related to stash success is handled in the
// parent update function.

// Note: mechanical stuff related to stash failure is handled in the parent
// update function.
Expand Down Expand Up @@ -848,14 +847,13 @@ func (m *stashModel) handleDocumentBrowsing(msg tea.Msg) tea.Cmd {
break
}

// Checks passed; perform the stash. Note that we optimistically
// show the status message.
// Checks passed; perform the stash.
m.common.filesStashed[md.stashID] = struct{}{}
m.common.filesStashing[md.stashID] = struct{}{}
m.common.latestFileStashed = md.stashID
cmds = append(cmds,
stashDocument(m.common.cc, *md),
m.newStatusMessage(stashedStatusMessage),
m.newStatusMessage(stashingStatusMessage),
)

// If we're stashing a filtered item, optimistically convert the
Expand All @@ -870,12 +868,9 @@ func (m *stashModel) handleDocumentBrowsing(msg tea.Msg) tea.Cmd {

// The spinner subtly shows the stash state in a non-optimistic
// fashion, namely because it was originally implemented this way.
// If this stash succeeds quickly enough, the spinner won't run
// at all.
if m.loadingDone() && !m.spinner.Visible() {
m.spinner.Start()
cmds = append(cmds, spinner.Tick)
}
// Ideally, if this stash succeeds quickly enough, the spinner
// wouldn't run at all.
cmds = append(cmds, m.spinner.Tick)

// Prompt for deletion
case "x":
Expand Down Expand Up @@ -1146,7 +1141,7 @@ func (m stashModel) view() string {
case stashStateReady:

loadingIndicator := " "
if !m.loadingDone() || m.spinner.Visible() {
if m.shouldSpin() {
loadingIndicator = m.spinner.View()
}

Expand Down
4 changes: 2 additions & 2 deletions ui/stashitem.go
Expand Up @@ -66,7 +66,7 @@ func stashItemView(b *strings.Builder, m stashModel, index int, md *markdown) {
date = dullYellowFg(date)
default:
if m.common.latestFileStashed == md.stashID &&
m.statusMessage == stashedStatusMessage {
m.statusMessage == stashingStatusMessage {
gutter = greenFg(verticalLine)
icon = dimGreenFg(icon)
title = greenFg(title)
Expand All @@ -90,7 +90,7 @@ func stashItemView(b *strings.Builder, m stashModel, index int, md *markdown) {
gutter = " "

if m.common.latestFileStashed == md.stashID &&
m.statusMessage == stashedStatusMessage {
m.statusMessage == stashingStatusMessage {
icon = dimGreenFg(icon)
title = greenFg(title)
date = semiDimGreenFg(date)
Expand Down
13 changes: 9 additions & 4 deletions ui/ui.go
Expand Up @@ -10,7 +10,6 @@ import (
"strings"
"time"

"github.com/charmbracelet/bubbles/spinner"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/charm"
"github.com/charmbracelet/charm/keygen"
Expand All @@ -25,6 +24,12 @@ const (
noteCharacterLimit = 256 // should match server
statusMessageTimeout = time.Second * 2 // how long to show status messages like "stashed!"
ellipsis = "…"

// Only show the spinner if it spins for at least this amount of time.
spinnerVisibilityTimeout = time.Millisecond * 140

// Minimum amount of time the spinner should be visible once it starts.
spinnerMinLifetime = time.Millisecond * 550
)

var (
Expand Down Expand Up @@ -188,8 +193,8 @@ func (m *model) unloadDocument() []tea.Cmd {
batch = append(batch, tea.ClearScrollArea)
}

if !m.stash.loadingDone() {
batch = append(batch, spinner.Tick)
if !m.stash.shouldSpin() {
batch = append(batch, m.stash.spinner.Tick)
}
return batch
}
Expand Down Expand Up @@ -230,7 +235,7 @@ func (m model) Init() tea.Cmd {
if d.Contains(StashedDoc) || d.Contains(NewsDoc) {
cmds = append(cmds,
newCharmClient,
spinner.Tick,
m.stash.spinner.Tick,
)
}

Expand Down

0 comments on commit db7f49b

Please sign in to comment.