glow/ui/stash.go

297 lines
6.5 KiB
Go
Raw Normal View History

package ui
import (
2020-05-13 23:02:39 +00:00
"fmt"
"sort"
"strconv"
2020-05-13 23:02:39 +00:00
"strings"
"github.com/charmbracelet/boba"
2020-05-15 00:23:11 +00:00
"github.com/charmbracelet/boba/paginator"
"github.com/charmbracelet/boba/spinner"
"github.com/charmbracelet/charm"
"github.com/charmbracelet/charm/ui/common"
2020-05-13 23:02:39 +00:00
"github.com/muesli/reflow/indent"
te "github.com/muesli/termenv"
)
2020-05-15 00:23:11 +00:00
const (
itemHeight = 3
topPadding = 3
2020-05-15 00:52:25 +00:00
bottomPadding = 4
2020-05-15 00:23:11 +00:00
)
// MSG
type stashErrMsg error
type stashSpinnerTickMsg struct{}
2020-05-14 02:08:17 +00:00
type gotStashMsg []*charm.Markdown
type gotStashedItemMsg *charm.Markdown
// MODEL
type stashState int
const (
stashStateInit stashState = iota
2020-05-13 23:02:39 +00:00
stashStateLoaded
2020-05-14 02:08:17 +00:00
stashStateLoadingItem
)
type stashModel struct {
2020-05-15 00:23:11 +00:00
cc *charm.Client
err error
state stashState
documents []*charm.Markdown
spinner spinner.Model
index int
terminalWidth int
terminalHeight int
// This handles the local pagination, which is different than the page
// we're fetching from on the server side
paginator paginator.Model
// Page we're fetching items from on the server, which is different from
// the local pagination. Generally, the server will return more items than
// we can display at a time so we can paginate locally without having to
// fetch every time.
page int
}
func (m *stashModel) SetSize(width, height int) {
m.terminalWidth = width
m.terminalHeight = height
// Update the paginator
perPage := (m.terminalHeight - topPadding - bottomPadding) / itemHeight
m.paginator.PerPage = perPage
}
// INIT
func stashInit(cc *charm.Client) (stashModel, boba.Cmd) {
s := spinner.NewModel()
s.Type = spinner.Dot
s.ForegroundColor = common.SpinnerColor
2020-05-15 00:23:11 +00:00
s.CustomMsgFunc = func() boba.Msg { return stashSpinnerTickMsg{} }
2020-05-15 00:52:25 +00:00
p := paginator.NewModel()
p.Type = paginator.Dots
p.InactiveDot = common.Subtle("•")
m := stashModel{
2020-05-15 00:23:11 +00:00
cc: cc,
spinner: s,
page: 1,
2020-05-15 00:52:25 +00:00
paginator: p,
}
return m, boba.Batch(
2020-05-14 02:08:17 +00:00
loadStash(m),
spinner.Tick(s),
)
}
// UPDATE
func stashUpdate(msg boba.Msg, m stashModel) (stashModel, boba.Cmd) {
2020-05-15 00:52:25 +00:00
var (
cmd boba.Cmd
cmds []boba.Cmd
)
switch msg := msg.(type) {
2020-05-14 00:21:37 +00:00
case boba.KeyMsg:
2020-05-15 00:23:11 +00:00
// Don't respond to keystrokes if we're still loading
if m.state == stashStateInit {
return m, nil
}
2020-05-14 00:21:37 +00:00
switch msg.String() {
case "k":
fallthrough
case "up":
m.index = max(0, m.index-1)
return m, nil
case "j":
fallthrough
case "down":
2020-05-15 00:52:25 +00:00
m.index = min(m.paginator.PerPage-1, m.index+1)
2020-05-14 00:21:37 +00:00
return m, nil
2020-05-14 02:08:17 +00:00
case "enter":
m.state = stashStateLoadingItem
return m, boba.Batch(
loadStashedItem(m.cc, m.documents[m.index].ID),
spinner.Tick(m.spinner),
)
2020-05-14 00:21:37 +00:00
}
case stashErrMsg:
m.err = msg
case gotStashMsg:
2020-05-13 23:02:39 +00:00
sort.Sort(charm.MarkdownsByCreatedAt(msg)) // sort by date
2020-05-15 00:23:11 +00:00
m.documents = append(m.documents, msg...)
2020-05-13 23:02:39 +00:00
m.state = stashStateLoaded
2020-05-15 00:23:11 +00:00
m.paginator.SetTotalPages(len(m.documents))
case stashSpinnerTickMsg:
2020-05-14 02:08:17 +00:00
if m.state == stashStateInit || m.state == stashStateLoadingItem {
m.spinner, cmd = spinner.Update(msg, m.spinner)
return m, cmd
}
}
2020-05-15 00:52:25 +00:00
if m.state == stashStateLoaded {
m.paginator, cmd = paginator.Update(msg, m.paginator)
cmds = append(cmds, cmd)
}
return m, boba.Batch(cmds...)
}
// VIEW
func stashView(m stashModel) string {
var s string
2020-05-13 23:02:39 +00:00
switch m.state {
case stashStateInit:
s += spinner.View(m.spinner) + " Loading stash..."
2020-05-14 02:08:17 +00:00
case stashStateLoadingItem:
s += spinner.View(m.spinner) + " Loading document..."
2020-05-13 23:02:39 +00:00
case stashStateLoaded:
if len(m.documents) == 0 {
s += stashEmtpyView(m)
break
}
2020-05-15 00:23:11 +00:00
// Blank lines we'll need to fill with newlines fo the viewport is
// properly filled
numBlankLines := (m.terminalHeight - topPadding - bottomPadding) % itemHeight
blankLines := ""
if numBlankLines > 0 {
blankLines = strings.Repeat("\n", numBlankLines)
}
2020-05-15 00:52:25 +00:00
s += fmt.Sprintf(
"Heres your markdown stash:\n\n%s\n\n%s%s\n\n%s",
stashPopulatedView(m), blankLines, paginator.View(m.paginator), helpView(m),
)
2020-05-13 23:02:39 +00:00
}
return "\n" + indent.String(s, 2)
}
func stashEmtpyView(m stashModel) string {
return "Nothing stashed yet."
}
func stashPopulatedView(m stashModel) string {
2020-05-15 00:52:25 +00:00
var s string
2020-05-15 00:23:11 +00:00
start, end := m.paginator.GetSliceBounds(len(m.documents))
docs := m.documents[start:end]
for i, v := range docs {
2020-05-14 00:21:37 +00:00
state := common.StateNormal
if i == m.index {
state = common.StateSelected
}
s += stashListItemView(*v).render(state) + "\n\n"
2020-05-13 23:02:39 +00:00
}
2020-05-15 00:52:25 +00:00
s = strings.TrimSpace(s) // trim final newlines
// If there aren't enough items to fill up this page (always the last page)
// then we need to add some newlines to fill up the space to push the
// footer stuff down elsewhere.
itemsOnPage := m.paginator.ItemsOnPage(len(m.documents))
if itemsOnPage < m.paginator.PerPage {
n := (m.paginator.PerPage - itemsOnPage) * itemHeight
s += strings.Repeat("\n", n)
}
return s
2020-05-15 00:23:11 +00:00
}
func helpView(m stashModel) string {
h := []string{"enter: open"}
if len(m.documents) > 0 {
h = append(h, "j/k, ↑/↓: choose")
}
if m.paginator.TotalPages > 1 {
h = append(h, "h/l, ←/→: page")
}
h = append(h, []string{"x: delete", "esc: exit"}...)
return common.HelpView(h...)
2020-05-13 23:02:39 +00:00
}
type stashListItemView charm.Markdown
2020-05-14 00:21:37 +00:00
func (m stashListItemView) render(state common.State) string {
line := common.VerticalLine(state) + " "
keyColor := common.NoColor
switch state {
case common.StateSelected:
keyColor = common.Fuschia
case common.StateDeleting:
keyColor = common.Red
}
titleKey := strconv.Itoa(m.ID)
if m.Note != "" {
titleKey += ":"
2020-05-14 00:21:37 +00:00
}
titleKey = te.String("#" + titleKey).Foreground(keyColor.Color()).String()
dateKey := te.String("Stashed:").Foreground(keyColor.Color()).String()
2020-05-13 23:02:39 +00:00
var s string
s += fmt.Sprintf("%s%s %s\n", line, titleKey, m.title(state))
s += fmt.Sprintf("%s%s %s", line, dateKey, m.date(state))
2020-05-13 23:02:39 +00:00
return s
}
2020-05-14 00:21:37 +00:00
func (m stashListItemView) date(state common.State) string {
c := common.Indigo
if state == common.StateDeleting {
c = common.FaintRed
}
2020-05-13 23:02:39 +00:00
s := m.CreatedAt.Format("02 Jan 2006 15:04:05 MST")
2020-05-14 00:21:37 +00:00
return te.String(s).Foreground(c.Color()).String()
2020-05-13 23:02:39 +00:00
}
2020-05-14 00:21:37 +00:00
func (m stashListItemView) title(state common.State) string {
2020-05-13 23:02:39 +00:00
if m.Note == "" {
return ""
}
2020-05-14 00:21:37 +00:00
c := common.Indigo
if state == common.StateDeleting {
c = common.Red
}
return te.String(m.Note).Foreground(c.Color()).String()
}
// CMD
2020-05-14 02:08:17 +00:00
func loadStash(m stashModel) boba.Cmd {
return func() boba.Msg {
stash, err := m.cc.GetStash(m.page)
if err != nil {
return stashErrMsg(err)
}
return gotStashMsg(stash)
}
}
2020-05-14 02:08:17 +00:00
func loadStashedItem(cc *charm.Client, id int) boba.Cmd {
return func() boba.Msg {
m, err := cc.GetStashMarkdown(id)
if err != nil {
return stashErrMsg(err)
}
return gotStashedItemMsg(m)
}
}