2020-05-13 16:53:58 +00:00
|
|
|
|
package ui
|
|
|
|
|
|
|
|
|
|
import (
|
2020-07-14 20:05:21 +00:00
|
|
|
|
"errors"
|
2020-05-13 23:02:39 +00:00
|
|
|
|
"fmt"
|
2020-07-14 20:05:21 +00:00
|
|
|
|
"io/ioutil"
|
2020-05-19 00:45:13 +00:00
|
|
|
|
"math"
|
2020-07-14 18:49:49 +00:00
|
|
|
|
"os"
|
2020-05-13 23:02:39 +00:00
|
|
|
|
"sort"
|
|
|
|
|
"strings"
|
2020-05-19 00:45:13 +00:00
|
|
|
|
"time"
|
2020-05-13 23:02:39 +00:00
|
|
|
|
|
2020-05-26 17:42:25 +00:00
|
|
|
|
"github.com/charmbracelet/bubbles/paginator"
|
|
|
|
|
"github.com/charmbracelet/bubbles/spinner"
|
|
|
|
|
"github.com/charmbracelet/bubbles/textinput"
|
|
|
|
|
tea "github.com/charmbracelet/bubbletea"
|
2020-05-13 16:53:58 +00:00
|
|
|
|
"github.com/charmbracelet/charm"
|
|
|
|
|
"github.com/charmbracelet/charm/ui/common"
|
2020-05-19 00:45:13 +00:00
|
|
|
|
"github.com/dustin/go-humanize"
|
2020-06-08 17:43:55 +00:00
|
|
|
|
runewidth "github.com/mattn/go-runewidth"
|
2020-05-13 23:02:39 +00:00
|
|
|
|
te "github.com/muesli/termenv"
|
2020-05-13 16:53:58 +00:00
|
|
|
|
)
|
|
|
|
|
|
2020-05-15 00:23:11 +00:00
|
|
|
|
const (
|
2020-05-22 02:29:46 +00:00
|
|
|
|
stashViewItemHeight = 3
|
2020-06-02 23:44:26 +00:00
|
|
|
|
stashViewTopPadding = 5
|
2020-05-22 02:29:46 +00:00
|
|
|
|
stashViewBottomPadding = 4
|
|
|
|
|
stashViewHorizontalPadding = 6
|
|
|
|
|
setNotePromptText = "Memo: "
|
2020-05-15 00:23:11 +00:00
|
|
|
|
)
|
|
|
|
|
|
2020-05-13 16:53:58 +00:00
|
|
|
|
// MSG
|
|
|
|
|
|
2020-05-14 02:08:17 +00:00
|
|
|
|
type gotStashMsg []*charm.Markdown
|
2020-05-21 19:14:33 +00:00
|
|
|
|
type gotNewsMsg []*charm.Markdown
|
2020-05-22 19:31:54 +00:00
|
|
|
|
type fetchedMarkdownMsg *markdown
|
2020-05-15 22:34:42 +00:00
|
|
|
|
type deletedStashedItemMsg int
|
2020-05-13 20:00:27 +00:00
|
|
|
|
|
2020-05-13 16:53:58 +00:00
|
|
|
|
// MODEL
|
|
|
|
|
|
2020-05-22 02:29:46 +00:00
|
|
|
|
// markdownType allows us to differentiate between the types of markdown
|
|
|
|
|
// documents we're dealing with, namely stuff the user stashed versus news.
|
2020-05-21 19:14:33 +00:00
|
|
|
|
type markdownType int
|
|
|
|
|
|
|
|
|
|
const (
|
2020-07-14 21:32:50 +00:00
|
|
|
|
stashedMarkdown markdownType = iota
|
2020-05-21 19:14:33 +00:00
|
|
|
|
newsMarkdown
|
2020-07-14 18:49:49 +00:00
|
|
|
|
localFile
|
2020-05-21 19:14:33 +00:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// markdown wraps charm.Markdown so we can differentiate between stashed items
|
2020-05-22 02:29:46 +00:00
|
|
|
|
// and news.
|
2020-05-21 19:14:33 +00:00
|
|
|
|
type markdown struct {
|
|
|
|
|
markdownType markdownType
|
2020-07-14 20:05:21 +00:00
|
|
|
|
localPath string // only relevent to local files
|
2020-05-21 19:14:33 +00:00
|
|
|
|
*charm.Markdown
|
|
|
|
|
}
|
|
|
|
|
|
2020-07-14 18:56:54 +00:00
|
|
|
|
// Sort documents with local files first, then by date
|
|
|
|
|
type markdownsByLocalFirst []*markdown
|
|
|
|
|
|
|
|
|
|
func (m markdownsByLocalFirst) Len() int { return len(m) }
|
|
|
|
|
func (m markdownsByLocalFirst) Swap(i, j int) { m[i], m[j] = m[j], m[i] }
|
|
|
|
|
func (m markdownsByLocalFirst) Less(i, j int) bool {
|
|
|
|
|
iType := m[i].markdownType
|
|
|
|
|
jType := m[j].markdownType
|
|
|
|
|
|
|
|
|
|
// Local files come first
|
|
|
|
|
if iType == localFile && jType != localFile {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
if iType != localFile && jType == localFile {
|
|
|
|
|
return false
|
|
|
|
|
}
|
2020-05-21 19:14:33 +00:00
|
|
|
|
|
2020-07-14 20:09:51 +00:00
|
|
|
|
// Both or neither are local files so sort by date descending
|
2020-07-14 18:56:54 +00:00
|
|
|
|
return m[i].CreatedAt.After(*m[j].CreatedAt)
|
|
|
|
|
}
|
2020-05-21 19:14:33 +00:00
|
|
|
|
|
2020-05-21 20:05:13 +00:00
|
|
|
|
type stashState int
|
|
|
|
|
|
2020-05-13 16:53:58 +00:00
|
|
|
|
const (
|
2020-05-13 20:00:27 +00:00
|
|
|
|
stashStateInit stashState = iota
|
2020-05-20 19:18:59 +00:00
|
|
|
|
stashStateReady
|
2020-05-15 22:34:42 +00:00
|
|
|
|
stashStatePromptDelete
|
2020-05-15 19:08:45 +00:00
|
|
|
|
stashStateLoadingDocument
|
2020-05-22 02:29:46 +00:00
|
|
|
|
stashStateSettingNote
|
2020-05-13 16:53:58 +00:00
|
|
|
|
)
|
|
|
|
|
|
2020-05-21 20:05:13 +00:00
|
|
|
|
type stashLoadedState byte
|
|
|
|
|
|
|
|
|
|
func (s stashLoadedState) done() bool {
|
|
|
|
|
return s&loadedStash != 0 && s&loadedNews != 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
loadedStash stashLoadedState = 1 << iota
|
|
|
|
|
loadedNews
|
2020-07-14 23:18:38 +00:00
|
|
|
|
loadedLocalFiles
|
2020-05-21 20:05:13 +00:00
|
|
|
|
)
|
|
|
|
|
|
2020-05-13 16:53:58 +00:00
|
|
|
|
type stashModel struct {
|
2020-05-15 00:23:11 +00:00
|
|
|
|
cc *charm.Client
|
|
|
|
|
state stashState
|
2020-05-22 22:42:18 +00:00
|
|
|
|
markdowns []*markdown
|
2020-05-15 00:23:11 +00:00
|
|
|
|
spinner spinner.Model
|
2020-05-22 02:29:46 +00:00
|
|
|
|
noteInput textinput.Model
|
2020-05-15 00:23:11 +00:00
|
|
|
|
terminalWidth int
|
|
|
|
|
terminalHeight int
|
2020-05-21 20:05:13 +00:00
|
|
|
|
loaded stashLoadedState // what's loaded? we find out with bitmasking
|
|
|
|
|
loading bool // are we currently loading something?
|
|
|
|
|
fullyLoaded bool // Have we loaded everything from the server?
|
2020-07-14 23:49:16 +00:00
|
|
|
|
hasStash bool // do we have stashed files to show?
|
|
|
|
|
hasLocalFiles bool // do we have local files to show?
|
|
|
|
|
hasNews bool // do we have news to show?
|
2020-05-15 00:23:11 +00:00
|
|
|
|
|
2020-05-21 19:14:33 +00:00
|
|
|
|
// This is just the index of the current page in view. To get the index
|
|
|
|
|
// of the selected item as it relates to the full set of documents we've
|
|
|
|
|
// fetched use the mardownIndex() method of this struct.
|
|
|
|
|
index int
|
|
|
|
|
|
2020-05-15 00:23:11 +00:00
|
|
|
|
// This handles the local pagination, which is different than the page
|
|
|
|
|
// we're fetching from on the server side
|
|
|
|
|
paginator paginator.Model
|
|
|
|
|
|
2020-05-21 19:14:33 +00:00
|
|
|
|
// Page we're fetching stash 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.
|
2020-05-15 00:23:11 +00:00
|
|
|
|
page int
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-19 17:20:39 +00:00
|
|
|
|
func (m *stashModel) setSize(width, height int) {
|
2020-05-15 00:23:11 +00:00
|
|
|
|
m.terminalWidth = width
|
|
|
|
|
m.terminalHeight = height
|
|
|
|
|
|
|
|
|
|
// Update the paginator
|
2020-05-19 00:10:28 +00:00
|
|
|
|
perPage := max(1, (m.terminalHeight-stashViewTopPadding-stashViewBottomPadding)/stashViewItemHeight)
|
2020-05-15 00:23:11 +00:00
|
|
|
|
m.paginator.PerPage = perPage
|
2020-05-22 22:42:18 +00:00
|
|
|
|
m.paginator.SetTotalPages(len(m.markdowns))
|
2020-05-15 02:48:38 +00:00
|
|
|
|
|
2020-05-22 02:29:46 +00:00
|
|
|
|
m.noteInput.Width = m.terminalWidth - stashViewHorizontalPadding*2 - len(setNotePromptText)
|
|
|
|
|
|
2020-05-15 02:48:38 +00:00
|
|
|
|
// Make sure the page stays in bounds
|
|
|
|
|
if m.paginator.Page >= m.paginator.TotalPages-1 {
|
2020-05-19 00:10:28 +00:00
|
|
|
|
m.paginator.Page = max(0, m.paginator.TotalPages-1)
|
2020-05-15 02:48:38 +00:00
|
|
|
|
}
|
2020-05-13 16:53:58 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-05-21 19:14:33 +00:00
|
|
|
|
// markdownIndex returns the index of the currently selected markdown item.
|
|
|
|
|
func (m stashModel) markdownIndex() int {
|
|
|
|
|
return m.paginator.Page*m.paginator.PerPage + m.index
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-22 20:28:15 +00:00
|
|
|
|
// return the current selected markdown in the stash
|
|
|
|
|
func (m stashModel) selectedMarkdown() *markdown {
|
2020-06-05 18:42:15 +00:00
|
|
|
|
if len(m.markdowns) == 0 || len(m.markdowns) <= m.markdownIndex() {
|
2020-06-02 22:58:15 +00:00
|
|
|
|
return nil
|
|
|
|
|
}
|
2020-05-22 22:42:18 +00:00
|
|
|
|
return m.markdowns[m.markdownIndex()]
|
2020-05-22 20:28:15 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-05-22 22:42:18 +00:00
|
|
|
|
// addDocuments adds markdown documents to the model
|
2020-05-22 20:01:23 +00:00
|
|
|
|
func (m *stashModel) addMarkdowns(mds ...*markdown) {
|
2020-05-22 22:42:18 +00:00
|
|
|
|
m.markdowns = append(m.markdowns, mds...)
|
2020-07-14 18:56:54 +00:00
|
|
|
|
sort.Sort(markdownsByLocalFirst(m.markdowns))
|
2020-05-22 22:42:18 +00:00
|
|
|
|
m.paginator.SetTotalPages(len(m.markdowns))
|
2020-05-22 20:01:23 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-05-13 16:53:58 +00:00
|
|
|
|
// INIT
|
|
|
|
|
|
2020-07-15 18:18:46 +00:00
|
|
|
|
func newStashModel() stashModel {
|
2020-05-13 16:53:58 +00:00
|
|
|
|
s := spinner.NewModel()
|
2020-07-14 22:54:32 +00:00
|
|
|
|
s.Frames = spinner.Dot
|
2020-05-13 16:53:58 +00:00
|
|
|
|
s.ForegroundColor = common.SpinnerColor
|
|
|
|
|
|
2020-05-15 00:52:25 +00:00
|
|
|
|
p := paginator.NewModel()
|
|
|
|
|
p.Type = paginator.Dots
|
|
|
|
|
p.InactiveDot = common.Subtle("•")
|
|
|
|
|
|
2020-05-22 02:29:46 +00:00
|
|
|
|
ni := textinput.NewModel()
|
|
|
|
|
ni.Prompt = te.String(setNotePromptText).Foreground(common.YellowGreen.Color()).String()
|
|
|
|
|
ni.CursorColor = common.Fuschia.String()
|
2020-05-22 22:42:18 +00:00
|
|
|
|
ni.CharLimit = noteCharacterLimit
|
2020-05-22 02:29:46 +00:00
|
|
|
|
ni.Focus()
|
|
|
|
|
|
2020-05-13 16:53:58 +00:00
|
|
|
|
m := stashModel{
|
2020-05-15 00:23:11 +00:00
|
|
|
|
spinner: s,
|
2020-05-22 02:29:46 +00:00
|
|
|
|
noteInput: ni,
|
2020-05-15 00:23:11 +00:00
|
|
|
|
page: 1,
|
2020-05-15 00:52:25 +00:00
|
|
|
|
paginator: p,
|
2020-05-13 16:53:58 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-07-15 18:18:46 +00:00
|
|
|
|
return m
|
2020-05-13 16:53:58 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// UPDATE
|
|
|
|
|
|
2020-05-26 17:42:25 +00:00
|
|
|
|
func stashUpdate(msg tea.Msg, m stashModel) (stashModel, tea.Cmd) {
|
2020-05-15 00:52:25 +00:00
|
|
|
|
var (
|
2020-05-26 17:42:25 +00:00
|
|
|
|
cmd tea.Cmd
|
|
|
|
|
cmds []tea.Cmd
|
2020-05-15 00:52:25 +00:00
|
|
|
|
)
|
|
|
|
|
|
2020-05-13 16:53:58 +00:00
|
|
|
|
switch msg := msg.(type) {
|
|
|
|
|
|
2020-07-14 23:18:38 +00:00
|
|
|
|
// We're finished searching for local files
|
|
|
|
|
case localFileSearchFinished:
|
|
|
|
|
m.loaded |= loadedLocalFiles
|
|
|
|
|
|
2020-05-21 20:05:13 +00:00
|
|
|
|
// Stash results have come in from the server
|
2020-05-13 16:53:58 +00:00
|
|
|
|
case gotStashMsg:
|
2020-05-15 23:16:22 +00:00
|
|
|
|
m.loading = false
|
|
|
|
|
|
|
|
|
|
if len(msg) == 0 {
|
2020-06-02 21:15:26 +00:00
|
|
|
|
// If the server comes back with nothing then we've got everything
|
2020-05-15 23:16:22 +00:00
|
|
|
|
m.fullyLoaded = true
|
2020-06-02 21:15:26 +00:00
|
|
|
|
} else {
|
2020-07-14 21:32:50 +00:00
|
|
|
|
docs := wrapMarkdowns(stashedMarkdown, msg)
|
2020-06-02 21:15:26 +00:00
|
|
|
|
m.addMarkdowns(docs...)
|
2020-07-14 23:49:16 +00:00
|
|
|
|
m.hasStash = true
|
2020-05-15 23:16:22 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-05-21 20:05:13 +00:00
|
|
|
|
m.loaded |= loadedStash
|
|
|
|
|
if m.loaded.done() {
|
|
|
|
|
m.state = stashStateReady
|
|
|
|
|
}
|
|
|
|
|
|
2020-07-14 23:18:38 +00:00
|
|
|
|
// News has come in from the server
|
2020-05-21 19:14:33 +00:00
|
|
|
|
case gotNewsMsg:
|
2020-05-21 20:05:13 +00:00
|
|
|
|
if len(msg) > 0 {
|
|
|
|
|
docs := wrapMarkdowns(newsMarkdown, msg)
|
2020-05-22 20:01:23 +00:00
|
|
|
|
m.addMarkdowns(docs...)
|
2020-07-14 23:49:16 +00:00
|
|
|
|
m.hasNews = true
|
2020-05-21 19:14:33 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-05-21 20:05:13 +00:00
|
|
|
|
m.loaded |= loadedNews
|
|
|
|
|
if m.loaded.done() {
|
|
|
|
|
m.state = stashStateReady
|
|
|
|
|
}
|
2020-05-21 19:14:33 +00:00
|
|
|
|
|
2020-06-22 19:11:48 +00:00
|
|
|
|
case spinner.TickMsg:
|
2020-05-15 19:08:45 +00:00
|
|
|
|
if m.state == stashStateInit || m.state == stashStateLoadingDocument {
|
2020-05-13 16:53:58 +00:00
|
|
|
|
m.spinner, cmd = spinner.Update(msg, m.spinner)
|
2020-05-15 22:39:23 +00:00
|
|
|
|
cmds = append(cmds, cmd)
|
2020-05-13 16:53:58 +00:00
|
|
|
|
}
|
2020-05-20 19:18:59 +00:00
|
|
|
|
|
2020-05-22 02:29:46 +00:00
|
|
|
|
// A note was set on a document. This may have happened in the pager, so
|
|
|
|
|
// we'll find the corresponding document here and update accordingly.
|
2020-05-20 19:18:59 +00:00
|
|
|
|
case noteSavedMsg:
|
2020-05-22 22:42:18 +00:00
|
|
|
|
for i := range m.markdowns {
|
|
|
|
|
if m.markdowns[i].ID == msg.ID {
|
|
|
|
|
m.markdowns[i].Note = msg.Note
|
2020-05-20 19:18:59 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
2020-05-13 16:53:58 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-05-22 02:29:46 +00:00
|
|
|
|
switch m.state {
|
|
|
|
|
case stashStateReady:
|
2020-05-26 17:42:25 +00:00
|
|
|
|
if msg, ok := msg.(tea.KeyMsg); ok {
|
2020-05-22 13:38:34 +00:00
|
|
|
|
switch msg.String() {
|
|
|
|
|
|
|
|
|
|
case "k":
|
|
|
|
|
fallthrough
|
|
|
|
|
case "up":
|
|
|
|
|
m.index--
|
|
|
|
|
if m.index < 0 && m.paginator.Page == 0 {
|
|
|
|
|
// Stop
|
|
|
|
|
m.index = 0
|
|
|
|
|
} else if m.index < 0 {
|
|
|
|
|
// Go to previous page
|
|
|
|
|
m.paginator.PrevPage()
|
2020-05-22 22:42:18 +00:00
|
|
|
|
m.index = m.paginator.ItemsOnPage(len(m.markdowns)) - 1
|
2020-05-22 13:38:34 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case "j":
|
|
|
|
|
fallthrough
|
|
|
|
|
case "down":
|
2020-05-22 22:42:18 +00:00
|
|
|
|
itemsOnPage := m.paginator.ItemsOnPage(len(m.markdowns))
|
2020-05-22 13:38:34 +00:00
|
|
|
|
m.index++
|
|
|
|
|
if m.index >= itemsOnPage && m.paginator.OnLastPage() {
|
|
|
|
|
// Stop
|
|
|
|
|
m.index = itemsOnPage - 1
|
|
|
|
|
} else if m.index >= itemsOnPage {
|
|
|
|
|
// Go to next page
|
|
|
|
|
m.index = 0
|
|
|
|
|
m.paginator.NextPage()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Open document
|
|
|
|
|
case "enter":
|
2020-06-03 16:46:53 +00:00
|
|
|
|
if len(m.markdowns) == 0 {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-22 13:38:34 +00:00
|
|
|
|
// Load the document from the server. We'll handle the message
|
|
|
|
|
// that comes back in the main update function.
|
|
|
|
|
m.state = stashStateLoadingDocument
|
2020-05-22 20:28:15 +00:00
|
|
|
|
md := m.selectedMarkdown()
|
2020-05-22 19:31:54 +00:00
|
|
|
|
|
2020-07-14 20:05:21 +00:00
|
|
|
|
if md.markdownType == localFile {
|
|
|
|
|
cmd = loadLocalMarkdown(md)
|
|
|
|
|
} else {
|
|
|
|
|
cmd = loadRemoteMarkdown(m.cc, md.ID, md.markdownType)
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-22 13:38:34 +00:00
|
|
|
|
cmds = append(cmds,
|
2020-07-14 20:05:21 +00:00
|
|
|
|
cmd,
|
2020-05-22 13:38:34 +00:00
|
|
|
|
spinner.Tick(m.spinner),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Set note
|
2020-06-03 18:24:40 +00:00
|
|
|
|
case "m":
|
2020-05-22 20:28:15 +00:00
|
|
|
|
md := m.selectedMarkdown()
|
2020-07-14 21:32:50 +00:00
|
|
|
|
isUserMarkdown := md.markdownType == stashedMarkdown
|
2020-05-22 20:28:15 +00:00
|
|
|
|
isSettingNote := m.state == stashStateSettingNote
|
|
|
|
|
isPromptingDelete := m.state == stashStatePromptDelete
|
|
|
|
|
|
|
|
|
|
if isUserMarkdown && !isSettingNote && !isPromptingDelete {
|
2020-05-22 13:38:34 +00:00
|
|
|
|
m.state = stashStateSettingNote
|
2020-05-22 20:28:15 +00:00
|
|
|
|
m.noteInput.SetValue(md.Note)
|
2020-05-22 13:38:34 +00:00
|
|
|
|
m.noteInput.CursorEnd()
|
|
|
|
|
return m, textinput.Blink(m.noteInput)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Prompt for deletion
|
|
|
|
|
case "x":
|
2020-07-14 21:32:50 +00:00
|
|
|
|
isUserMarkdown := m.selectedMarkdown().markdownType == stashedMarkdown
|
2020-05-22 13:38:34 +00:00
|
|
|
|
isValidState := m.state != stashStateSettingNote
|
2020-05-22 20:28:15 +00:00
|
|
|
|
|
2020-05-22 13:38:34 +00:00
|
|
|
|
if isUserMarkdown && isValidState {
|
|
|
|
|
m.state = stashStatePromptDelete
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-05-15 23:16:22 +00:00
|
|
|
|
|
|
|
|
|
// Update paginator
|
2020-05-15 00:52:25 +00:00
|
|
|
|
m.paginator, cmd = paginator.Update(msg, m.paginator)
|
|
|
|
|
cmds = append(cmds, cmd)
|
2020-05-15 21:31:43 +00:00
|
|
|
|
|
|
|
|
|
// Keep the index in bounds when paginating
|
2020-05-22 22:42:18 +00:00
|
|
|
|
itemsOnPage := m.paginator.ItemsOnPage(len(m.markdowns))
|
2020-05-15 21:31:43 +00:00
|
|
|
|
if m.index > itemsOnPage-1 {
|
|
|
|
|
m.index = itemsOnPage - 1
|
|
|
|
|
}
|
2020-05-15 23:16:22 +00:00
|
|
|
|
|
|
|
|
|
// If we're on the last page and we haven't loaded everything, get
|
|
|
|
|
// more stuff.
|
|
|
|
|
if m.paginator.OnLastPage() && !m.loading && !m.fullyLoaded {
|
|
|
|
|
m.page++
|
|
|
|
|
m.loading = true
|
|
|
|
|
cmds = append(cmds, loadStash(m))
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-22 13:38:34 +00:00
|
|
|
|
case stashStatePromptDelete:
|
2020-05-26 17:42:25 +00:00
|
|
|
|
if msg, ok := msg.(tea.KeyMsg); ok {
|
2020-05-22 13:38:34 +00:00
|
|
|
|
switch msg.String() {
|
|
|
|
|
|
|
|
|
|
// Confirm deletion
|
|
|
|
|
case "y":
|
|
|
|
|
if m.state != stashStatePromptDelete {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
i := m.markdownIndex()
|
2020-05-22 22:42:18 +00:00
|
|
|
|
id := m.markdowns[i].ID
|
2020-05-22 13:38:34 +00:00
|
|
|
|
|
|
|
|
|
// Delete optimistically and remove the stashed item
|
|
|
|
|
// before we've received a success response.
|
2020-05-22 22:42:18 +00:00
|
|
|
|
m.markdowns = append(m.markdowns[:i], m.markdowns[i+1:]...)
|
2020-05-22 13:38:34 +00:00
|
|
|
|
|
|
|
|
|
// Update pagination
|
2020-05-22 22:42:18 +00:00
|
|
|
|
m.paginator.SetTotalPages(len(m.markdowns))
|
2020-05-22 13:38:34 +00:00
|
|
|
|
m.paginator.Page = min(m.paginator.Page, m.paginator.TotalPages-1)
|
|
|
|
|
|
|
|
|
|
// Set state and delete
|
|
|
|
|
m.state = stashStateReady
|
|
|
|
|
return m, deleteStashedItem(m.cc, id)
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
m.state = stashStateReady
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-22 02:29:46 +00:00
|
|
|
|
case stashStateSettingNote:
|
|
|
|
|
|
2020-05-26 17:42:25 +00:00
|
|
|
|
if msg, ok := msg.(tea.KeyMsg); ok {
|
2020-05-22 02:29:46 +00:00
|
|
|
|
switch msg.String() {
|
|
|
|
|
case "esc":
|
|
|
|
|
// Cancel note
|
|
|
|
|
m.state = stashStateReady
|
|
|
|
|
m.noteInput.Reset()
|
|
|
|
|
case "enter":
|
|
|
|
|
// Set new note
|
2020-05-22 22:42:18 +00:00
|
|
|
|
md := m.selectedMarkdown()
|
2020-05-22 02:29:46 +00:00
|
|
|
|
newNote := m.noteInput.Value()
|
2020-05-22 22:42:18 +00:00
|
|
|
|
cmd = saveDocumentNote(m.cc, md.ID, newNote)
|
|
|
|
|
md.Note = newNote
|
2020-05-22 02:29:46 +00:00
|
|
|
|
m.noteInput.Reset()
|
|
|
|
|
m.state = stashStateReady
|
|
|
|
|
return m, cmd
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update the text input component used to set notes
|
|
|
|
|
m.noteInput, cmd = textinput.Update(msg, m.noteInput)
|
|
|
|
|
cmds = append(cmds, cmd)
|
|
|
|
|
|
2020-05-15 00:52:25 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-05-15 22:34:42 +00:00
|
|
|
|
// If an item is being confirmed for delete, any key (other than the key
|
|
|
|
|
// used for confirmation above) cancels the deletion
|
|
|
|
|
|
2020-05-26 17:42:25 +00:00
|
|
|
|
return m, tea.Batch(cmds...)
|
2020-05-13 16:53:58 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// VIEW
|
|
|
|
|
|
|
|
|
|
func stashView(m stashModel) string {
|
|
|
|
|
var s string
|
2020-05-13 23:02:39 +00:00
|
|
|
|
switch m.state {
|
|
|
|
|
case stashStateInit:
|
2020-05-26 16:36:14 +00:00
|
|
|
|
s += " " + spinner.View(m.spinner) + " Loading stash..."
|
2020-05-15 19:08:45 +00:00
|
|
|
|
case stashStateLoadingDocument:
|
2020-05-26 16:36:14 +00:00
|
|
|
|
s += " " + spinner.View(m.spinner) + " Loading document..."
|
2020-05-20 19:18:59 +00:00
|
|
|
|
case stashStateReady:
|
2020-05-15 22:34:42 +00:00
|
|
|
|
fallthrough
|
2020-05-22 02:29:46 +00:00
|
|
|
|
case stashStateSettingNote:
|
|
|
|
|
fallthrough
|
2020-05-15 22:34:42 +00:00
|
|
|
|
case stashStatePromptDelete:
|
2020-06-02 22:58:15 +00:00
|
|
|
|
|
2020-05-21 19:14:33 +00:00
|
|
|
|
// We need to fill any empty height with newlines so the footer reaches
|
|
|
|
|
// the bottom.
|
2020-06-02 23:44:26 +00:00
|
|
|
|
numBlankLines := max(0, (m.terminalHeight-stashViewTopPadding-stashViewBottomPadding)%stashViewItemHeight)
|
2020-05-15 00:23:11 +00:00
|
|
|
|
blankLines := ""
|
|
|
|
|
if numBlankLines > 0 {
|
|
|
|
|
blankLines = strings.Repeat("\n", numBlankLines)
|
|
|
|
|
}
|
|
|
|
|
|
2020-06-02 22:58:15 +00:00
|
|
|
|
var header string
|
2020-07-14 23:49:16 +00:00
|
|
|
|
if m.hasStash && m.hasLocalFiles {
|
|
|
|
|
header = "Here are your local and stashed markdown files:"
|
|
|
|
|
} else if m.hasStash {
|
2020-06-02 22:58:15 +00:00
|
|
|
|
header = "Here’s your markdown stash:"
|
2020-07-14 23:49:16 +00:00
|
|
|
|
} else if m.hasLocalFiles {
|
|
|
|
|
header = "Here are your local markdown files:"
|
|
|
|
|
} else if m.hasNews {
|
|
|
|
|
// TODO: proper help
|
|
|
|
|
header = "Here are some Glow updates:"
|
2020-06-02 22:58:15 +00:00
|
|
|
|
} else {
|
2020-07-14 23:49:16 +00:00
|
|
|
|
// TODO: proper help
|
2020-06-02 22:58:15 +00:00
|
|
|
|
header = "Nothing stashed yet. To stash you can " + common.Code("glow stash path/to/file.md") + "."
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-22 02:29:46 +00:00
|
|
|
|
switch m.state {
|
|
|
|
|
case stashStatePromptDelete:
|
2020-07-15 16:07:15 +00:00
|
|
|
|
header = redFg("Delete this item? ") + faintRedFg("(y/N)")
|
2020-05-22 02:29:46 +00:00
|
|
|
|
case stashStateSettingNote:
|
2020-07-15 16:07:15 +00:00
|
|
|
|
header = yellowFg("Set the memo for this item?")
|
2020-05-15 22:34:42 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-05-15 23:16:22 +00:00
|
|
|
|
var pagination string
|
2020-05-15 22:44:22 +00:00
|
|
|
|
if m.paginator.TotalPages > 1 {
|
2020-05-15 23:16:22 +00:00
|
|
|
|
pagination = paginator.View(m.paginator)
|
|
|
|
|
|
|
|
|
|
if !m.fullyLoaded {
|
2020-05-18 22:58:19 +00:00
|
|
|
|
pagination += common.Subtle(" ···")
|
2020-05-15 23:16:22 +00:00
|
|
|
|
}
|
2020-05-15 22:44:22 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-05-15 00:52:25 +00:00
|
|
|
|
s += fmt.Sprintf(
|
2020-05-26 15:51:08 +00:00
|
|
|
|
" %s\n\n %s\n\n%s\n\n%s %s\n\n %s",
|
2020-05-15 01:12:13 +00:00
|
|
|
|
glowLogoView(" Glow "),
|
2020-05-15 22:34:42 +00:00
|
|
|
|
header,
|
2020-06-02 23:44:26 +00:00
|
|
|
|
stashPopulatedView(m),
|
2020-05-15 01:12:13 +00:00
|
|
|
|
blankLines,
|
2020-05-15 23:16:22 +00:00
|
|
|
|
pagination,
|
2020-05-21 19:14:33 +00:00
|
|
|
|
stashHelpView(m),
|
2020-05-15 00:52:25 +00:00
|
|
|
|
)
|
2020-05-13 23:02:39 +00:00
|
|
|
|
}
|
2020-05-27 15:55:00 +00:00
|
|
|
|
return "\n" + indent(s, 1)
|
2020-05-13 23:02:39 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-05-19 17:20:39 +00:00
|
|
|
|
func glowLogoView(text string) string {
|
|
|
|
|
return te.String(text).
|
|
|
|
|
Bold().
|
|
|
|
|
Foreground(glowLogoTextColor).
|
|
|
|
|
Background(common.Fuschia.Color()).
|
|
|
|
|
String()
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-13 23:02:39 +00:00
|
|
|
|
func stashEmtpyView(m stashModel) string {
|
|
|
|
|
return "Nothing stashed yet."
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func stashPopulatedView(m stashModel) string {
|
2020-05-26 16:36:14 +00:00
|
|
|
|
var b strings.Builder
|
2020-05-15 00:52:25 +00:00
|
|
|
|
|
2020-06-02 23:44:26 +00:00
|
|
|
|
if len(m.markdowns) > 0 {
|
|
|
|
|
start, end := m.paginator.GetSliceBounds(len(m.markdowns))
|
|
|
|
|
docs := m.markdowns[start:end]
|
2020-05-15 00:23:11 +00:00
|
|
|
|
|
2020-06-02 23:44:26 +00:00
|
|
|
|
for i, md := range docs {
|
|
|
|
|
stashItemView(&b, m, i, md)
|
|
|
|
|
if i != len(docs)-1 {
|
|
|
|
|
fmt.Fprintf(&b, "\n\n")
|
|
|
|
|
}
|
2020-05-26 16:36:14 +00:00
|
|
|
|
}
|
2020-05-13 23:02:39 +00:00
|
|
|
|
}
|
2020-05-15 00:52:25 +00:00
|
|
|
|
|
|
|
|
|
// If there aren't enough items to fill up this page (always the last page)
|
2020-06-02 23:44:26 +00:00
|
|
|
|
// then we need to add some newlines to fill up the space where stash items
|
|
|
|
|
// would have been.
|
2020-05-22 22:42:18 +00:00
|
|
|
|
itemsOnPage := m.paginator.ItemsOnPage(len(m.markdowns))
|
2020-05-15 00:52:25 +00:00
|
|
|
|
if itemsOnPage < m.paginator.PerPage {
|
2020-05-18 18:37:50 +00:00
|
|
|
|
n := (m.paginator.PerPage - itemsOnPage) * stashViewItemHeight
|
2020-06-02 23:44:26 +00:00
|
|
|
|
if len(m.markdowns) == 0 {
|
|
|
|
|
n -= stashViewItemHeight - 1
|
|
|
|
|
}
|
2020-05-26 16:36:14 +00:00
|
|
|
|
for i := 0; i < n; i++ {
|
|
|
|
|
fmt.Fprint(&b, "\n")
|
|
|
|
|
}
|
2020-05-15 00:52:25 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-05-26 16:36:14 +00:00
|
|
|
|
return b.String()
|
2020-05-15 00:23:11 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-05-21 19:14:33 +00:00
|
|
|
|
func stashHelpView(m stashModel) string {
|
|
|
|
|
var (
|
2020-07-14 20:16:26 +00:00
|
|
|
|
h []string
|
|
|
|
|
md = m.selectedMarkdown()
|
2020-07-14 21:32:50 +00:00
|
|
|
|
isStashed = md != nil && md.markdownType == stashedMarkdown
|
2020-05-21 19:14:33 +00:00
|
|
|
|
)
|
|
|
|
|
|
2020-05-22 02:29:46 +00:00
|
|
|
|
if m.state == stashStateSettingNote {
|
|
|
|
|
h = append(h, "enter: confirm", "esc: cancel")
|
|
|
|
|
} else if m.state == stashStatePromptDelete {
|
2020-05-15 22:34:42 +00:00
|
|
|
|
h = append(h, "y: delete", "n: cancel")
|
|
|
|
|
} else {
|
2020-05-22 22:42:18 +00:00
|
|
|
|
if len(m.markdowns) > 0 {
|
2020-06-03 16:46:53 +00:00
|
|
|
|
h = append(h, "enter: open")
|
2020-05-15 22:34:42 +00:00
|
|
|
|
h = append(h, "j/k, ↑/↓: choose")
|
|
|
|
|
}
|
|
|
|
|
if m.paginator.TotalPages > 1 {
|
|
|
|
|
h = append(h, "h/l, ←/→: page")
|
|
|
|
|
}
|
2020-07-14 20:16:26 +00:00
|
|
|
|
if isStashed && len(m.markdowns) > 0 {
|
2020-06-03 18:24:40 +00:00
|
|
|
|
h = append(h, []string{"x: delete", "m: set memo"}...)
|
2020-05-21 19:14:33 +00:00
|
|
|
|
}
|
|
|
|
|
h = append(h, []string{"esc: exit"}...)
|
2020-05-15 00:23:11 +00:00
|
|
|
|
}
|
|
|
|
|
return common.HelpView(h...)
|
2020-05-13 23:02:39 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-05-13 16:53:58 +00:00
|
|
|
|
// CMD
|
|
|
|
|
|
2020-07-14 20:05:21 +00:00
|
|
|
|
func loadRemoteMarkdown(cc *charm.Client, id int, t markdownType) tea.Cmd {
|
2020-05-26 17:42:25 +00:00
|
|
|
|
return func() tea.Msg {
|
2020-05-22 19:31:54 +00:00
|
|
|
|
var (
|
|
|
|
|
md *charm.Markdown
|
|
|
|
|
err error
|
|
|
|
|
)
|
2020-07-14 20:05:21 +00:00
|
|
|
|
|
2020-07-14 21:32:50 +00:00
|
|
|
|
if t == stashedMarkdown {
|
2020-05-22 19:31:54 +00:00
|
|
|
|
md, err = cc.GetStashMarkdown(id)
|
|
|
|
|
} else {
|
|
|
|
|
md, err = cc.GetNewsMarkdown(id)
|
|
|
|
|
}
|
2020-07-14 20:05:21 +00:00
|
|
|
|
|
2020-05-21 19:14:33 +00:00
|
|
|
|
if err != nil {
|
2020-05-22 20:06:48 +00:00
|
|
|
|
return errMsg(err)
|
2020-05-21 19:14:33 +00:00
|
|
|
|
}
|
2020-07-14 20:05:21 +00:00
|
|
|
|
|
2020-05-22 19:31:54 +00:00
|
|
|
|
return fetchedMarkdownMsg(&markdown{
|
2020-05-22 20:28:15 +00:00
|
|
|
|
markdownType: t,
|
2020-05-21 19:14:33 +00:00
|
|
|
|
Markdown: md,
|
|
|
|
|
})
|
2020-05-14 02:08:17 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
2020-05-15 22:34:42 +00:00
|
|
|
|
|
2020-07-14 20:05:21 +00:00
|
|
|
|
func loadLocalMarkdown(md *markdown) tea.Cmd {
|
|
|
|
|
return func() tea.Msg {
|
|
|
|
|
if md.markdownType != localFile {
|
|
|
|
|
return errMsg(errors.New("could not load local file: not a local file"))
|
|
|
|
|
}
|
|
|
|
|
if md.localPath == "" {
|
|
|
|
|
return errMsg(errors.New("could not load file: missing path"))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
data, err := ioutil.ReadFile(md.localPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return errMsg(err)
|
|
|
|
|
}
|
|
|
|
|
md.Body = string(data)
|
|
|
|
|
return fetchedMarkdownMsg(md)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-26 17:42:25 +00:00
|
|
|
|
func deleteStashedItem(cc *charm.Client, id int) tea.Cmd {
|
|
|
|
|
return func() tea.Msg {
|
2020-05-15 22:34:42 +00:00
|
|
|
|
err := cc.DeleteMarkdown(id)
|
|
|
|
|
if err != nil {
|
2020-05-22 20:06:48 +00:00
|
|
|
|
return errMsg(err)
|
2020-05-15 22:34:42 +00:00
|
|
|
|
}
|
|
|
|
|
return deletedStashedItemMsg(id)
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-05-18 20:08:49 +00:00
|
|
|
|
|
|
|
|
|
// ETC
|
|
|
|
|
|
2020-05-22 02:29:46 +00:00
|
|
|
|
// wrapMarkdowns wraps a *charm.Markdown with a *markdown in order to add some
|
|
|
|
|
// extra metadata.
|
2020-05-21 19:14:33 +00:00
|
|
|
|
func wrapMarkdowns(t markdownType, md []*charm.Markdown) (m []*markdown) {
|
|
|
|
|
for _, v := range md {
|
|
|
|
|
m = append(m, &markdown{
|
|
|
|
|
markdownType: t,
|
|
|
|
|
Markdown: v,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
return m
|
|
|
|
|
}
|
|
|
|
|
|
2020-07-14 19:35:02 +00:00
|
|
|
|
// Convert path to local file to Markdown. Note that we could be doing things
|
|
|
|
|
// like checking if the file is a directory, but we trust that gitcha has
|
|
|
|
|
// already done that.
|
|
|
|
|
func localFileToMarkdown(cwd, path string) (*markdown, error) {
|
|
|
|
|
md := &markdown{
|
|
|
|
|
markdownType: localFile,
|
2020-07-14 20:05:21 +00:00
|
|
|
|
localPath: path,
|
2020-07-14 19:35:02 +00:00
|
|
|
|
Markdown: &charm.Markdown{},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Strip absolute path
|
|
|
|
|
md.Markdown.Note = strings.Replace(path, cwd+"/", "", -1)
|
|
|
|
|
|
|
|
|
|
// Get last modified time
|
|
|
|
|
info, err := os.Stat(path)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
t := info.ModTime()
|
|
|
|
|
md.CreatedAt = &t
|
|
|
|
|
|
|
|
|
|
return md, nil
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-18 20:08:49 +00:00
|
|
|
|
func truncate(str string, num int) string {
|
2020-06-08 17:43:55 +00:00
|
|
|
|
return runewidth.Truncate(str, num, "…")
|
2020-05-18 20:08:49 +00:00
|
|
|
|
}
|
2020-05-19 00:45:13 +00:00
|
|
|
|
|
|
|
|
|
var magnitudes = []humanize.RelTimeMagnitude{
|
|
|
|
|
{D: time.Second, Format: "now", DivBy: time.Second},
|
|
|
|
|
{D: 2 * time.Second, Format: "1 second %s", DivBy: 1},
|
|
|
|
|
{D: time.Minute, Format: "%d seconds %s", DivBy: time.Second},
|
|
|
|
|
{D: 2 * time.Minute, Format: "1 minute %s", DivBy: 1},
|
|
|
|
|
{D: time.Hour, Format: "%d minutes %s", DivBy: time.Minute},
|
|
|
|
|
{D: 2 * time.Hour, Format: "1 hour %s", DivBy: 1},
|
|
|
|
|
{D: humanize.Day, Format: "%d hours %s", DivBy: time.Hour},
|
|
|
|
|
{D: 2 * humanize.Day, Format: "1 day %s", DivBy: 1},
|
|
|
|
|
{D: humanize.Week, Format: "%d days %s", DivBy: humanize.Day},
|
|
|
|
|
{D: 2 * humanize.Week, Format: "1 week %s", DivBy: 1},
|
|
|
|
|
{D: humanize.Month, Format: "%d weeks %s", DivBy: humanize.Week},
|
|
|
|
|
{D: 2 * humanize.Month, Format: "1 month %s", DivBy: 1},
|
|
|
|
|
{D: humanize.Year, Format: "%d months %s", DivBy: humanize.Month},
|
|
|
|
|
{D: 18 * humanize.Month, Format: "1 year %s", DivBy: 1},
|
|
|
|
|
{D: 2 * humanize.Year, Format: "2 years %s", DivBy: 1},
|
|
|
|
|
{D: humanize.LongTime, Format: "%d years %s", DivBy: humanize.Year},
|
|
|
|
|
{D: math.MaxInt64, Format: "a long while %s", DivBy: 1},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func relativeTime(then time.Time) string {
|
|
|
|
|
now := time.Now()
|
|
|
|
|
if now.Sub(then) < humanize.Week {
|
|
|
|
|
return humanize.CustomRelTime(then, now, "ago", "from now", magnitudes)
|
|
|
|
|
}
|
|
|
|
|
return then.Format("02 Jan 2006 15:04 MST")
|
|
|
|
|
}
|