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-08-18 19:41:57 +00:00
|
|
|
|
"log"
|
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"
|
2020-12-11 01:43:31 +00:00
|
|
|
|
lib "github.com/charmbracelet/charm/ui/common"
|
2020-07-20 15:00:17 +00:00
|
|
|
|
"github.com/muesli/reflow/ansi"
|
2020-12-14 23:54:39 +00:00
|
|
|
|
"github.com/muesli/reflow/truncate"
|
2020-05-13 23:02:39 +00:00
|
|
|
|
te "github.com/muesli/termenv"
|
2020-11-16 20:25:23 +00:00
|
|
|
|
"github.com/sahilm/fuzzy"
|
2020-05-13 16:53:58 +00:00
|
|
|
|
)
|
|
|
|
|
|
2020-05-15 00:23:11 +00:00
|
|
|
|
const (
|
2020-07-16 19:09:06 +00:00
|
|
|
|
stashIndent = 1
|
2020-11-30 23:44:58 +00:00
|
|
|
|
stashViewItemHeight = 3 // height of stash entry, including gap
|
|
|
|
|
stashViewTopPadding = 5 // logo, status bar, gaps
|
|
|
|
|
stashViewBottomPadding = 3 // pagination and gaps, but not help
|
2020-05-22 02:29:46 +00:00
|
|
|
|
stashViewHorizontalPadding = 6
|
2020-12-14 21:26:29 +00:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
stashedStatusMessage = statusMessage{normalStatusMessage, "Stashed!"}
|
|
|
|
|
alreadyStashedStatusMessage = statusMessage{subtleStatusMessage, "Already stashed"}
|
2020-05-15 00:23:11 +00:00
|
|
|
|
)
|
|
|
|
|
|
2020-07-28 20:41:44 +00:00
|
|
|
|
var (
|
2020-12-11 01:43:31 +00:00
|
|
|
|
stashTextInputPromptStyle styleFunc = newFgStyle(lib.YellowGreen)
|
2020-12-01 01:32:48 +00:00
|
|
|
|
dividerDot string = darkGrayFg(" • ")
|
2020-12-03 23:15:59 +00:00
|
|
|
|
dividerBar string = darkGrayFg(" │ ")
|
2020-12-01 01:32:48 +00:00
|
|
|
|
offlineHeaderNote string = darkGrayFg("(Offline)")
|
2020-07-28 20:41:44 +00:00
|
|
|
|
)
|
|
|
|
|
|
2020-05-13 16:53:58 +00:00
|
|
|
|
// MSG
|
|
|
|
|
|
2020-05-15 22:34:42 +00:00
|
|
|
|
type deletedStashedItemMsg int
|
2020-11-23 21:56:44 +00:00
|
|
|
|
type filteredMarkdownMsg []*markdown
|
2020-12-10 01:12:01 +00:00
|
|
|
|
type fetchedMarkdownMsg *markdown
|
|
|
|
|
|
|
|
|
|
type markdownFetchFailedMsg struct {
|
|
|
|
|
err error
|
|
|
|
|
id int
|
|
|
|
|
note string
|
|
|
|
|
}
|
2020-05-13 20:00:27 +00:00
|
|
|
|
|
2020-05-13 16:53:58 +00:00
|
|
|
|
// MODEL
|
|
|
|
|
|
2020-12-11 03:53:09 +00:00
|
|
|
|
// stashViewState is the high-level state of the file listing.
|
2020-11-26 03:49:49 +00:00
|
|
|
|
type stashViewState int
|
2020-05-21 20:05:13 +00:00
|
|
|
|
|
2020-05-13 16:53:58 +00:00
|
|
|
|
const (
|
2020-11-26 03:49:49 +00:00
|
|
|
|
stashStateReady stashViewState = iota
|
2020-05-15 19:08:45 +00:00
|
|
|
|
stashStateLoadingDocument
|
2020-07-16 19:54:55 +00:00
|
|
|
|
stashStateShowingError
|
2020-05-13 16:53:58 +00:00
|
|
|
|
)
|
|
|
|
|
|
2020-12-03 23:15:59 +00:00
|
|
|
|
// The types of documents we are currently showing to the user.
|
2020-12-08 23:38:10 +00:00
|
|
|
|
type sectionKey int
|
2020-11-28 23:30:51 +00:00
|
|
|
|
|
|
|
|
|
const (
|
2020-12-08 23:38:10 +00:00
|
|
|
|
localSection = iota
|
|
|
|
|
stashedSection
|
|
|
|
|
newsSection
|
2020-11-28 23:30:51 +00:00
|
|
|
|
)
|
|
|
|
|
|
2020-12-11 03:53:09 +00:00
|
|
|
|
// section contains definitions and state information for displaying a tab and
|
|
|
|
|
// its contents in the file listing view.
|
2020-12-08 23:38:10 +00:00
|
|
|
|
type section struct {
|
|
|
|
|
key sectionKey
|
|
|
|
|
docTypes DocTypeSet
|
|
|
|
|
paginator paginator.Model
|
|
|
|
|
cursor int
|
2020-12-03 23:15:59 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-12-11 03:53:09 +00:00
|
|
|
|
// map sections to their associated types.
|
2020-12-08 23:38:10 +00:00
|
|
|
|
var sections = map[sectionKey]section{
|
|
|
|
|
localSection: {
|
|
|
|
|
key: localSection,
|
|
|
|
|
docTypes: NewDocTypeSet(LocalDoc),
|
|
|
|
|
},
|
|
|
|
|
stashedSection: {
|
|
|
|
|
key: stashedSection,
|
|
|
|
|
docTypes: NewDocTypeSet(StashedDoc, ConvertedDoc),
|
|
|
|
|
},
|
|
|
|
|
newsSection: {
|
|
|
|
|
key: newsSection,
|
|
|
|
|
docTypes: NewDocTypeSet(NewsDoc),
|
|
|
|
|
},
|
2020-12-03 23:15:59 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-12-11 03:53:09 +00:00
|
|
|
|
// filterState is the current filtering state in the file listing.
|
2020-11-26 01:15:21 +00:00
|
|
|
|
type filterState int
|
|
|
|
|
|
|
|
|
|
const (
|
2020-11-28 23:30:51 +00:00
|
|
|
|
unfiltered filterState = iota // no filter set
|
|
|
|
|
filtering // user is actively setting a filter
|
|
|
|
|
filterApplied // a filter is applied and user is not editing filter
|
2020-11-26 01:15:21 +00:00
|
|
|
|
)
|
|
|
|
|
|
2020-12-11 03:53:09 +00:00
|
|
|
|
// selectionState is the state of the currently selected document.
|
2020-11-26 03:49:49 +00:00
|
|
|
|
type selectionState int
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
selectionIdle = iota
|
|
|
|
|
selectionSettingNote
|
|
|
|
|
selectionPromptingDelete
|
|
|
|
|
)
|
|
|
|
|
|
2020-12-14 21:26:29 +00:00
|
|
|
|
// statusMessageType adds some context to the status message being sent.
|
|
|
|
|
type statusMessageType int
|
|
|
|
|
|
|
|
|
|
// Types of status messages.
|
|
|
|
|
const (
|
|
|
|
|
normalStatusMessage statusMessageType = iota
|
|
|
|
|
subtleStatusMessage
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// statusMessage is an ephemeral note displayed in the UI.
|
|
|
|
|
type statusMessage struct {
|
|
|
|
|
status statusMessageType
|
|
|
|
|
message string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// String returns a styled version of the status message appropriate for the
|
|
|
|
|
// given context.
|
|
|
|
|
func (s statusMessage) String() string {
|
|
|
|
|
switch s.status {
|
|
|
|
|
case subtleStatusMessage:
|
|
|
|
|
return dimGreenFg(s.message)
|
|
|
|
|
default:
|
|
|
|
|
return greenFg(s.message)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-13 16:53:58 +00:00
|
|
|
|
type stashModel struct {
|
2020-12-12 00:54:19 +00:00
|
|
|
|
common *commonModel
|
|
|
|
|
err error
|
|
|
|
|
spinner spinner.Model
|
|
|
|
|
noteInput textinput.Model
|
|
|
|
|
filterInput textinput.Model
|
|
|
|
|
stashFullyLoaded bool // have we loaded all available stashed documents from the server?
|
|
|
|
|
viewState stashViewState
|
|
|
|
|
filterState filterState
|
|
|
|
|
selectionState selectionState
|
|
|
|
|
showFullHelp bool
|
|
|
|
|
showStatusMessage bool
|
2020-12-14 21:26:29 +00:00
|
|
|
|
statusMessage statusMessage
|
2020-12-12 00:54:19 +00:00
|
|
|
|
statusMessageTimer *time.Timer
|
|
|
|
|
stashStatusMessageQueued bool
|
2020-11-28 23:30:51 +00:00
|
|
|
|
|
2020-12-08 23:38:10 +00:00
|
|
|
|
// Available document sections we can cycle through. We use a slice, rather
|
|
|
|
|
// than a map, because order is important.
|
|
|
|
|
sections []section
|
2020-11-28 23:30:51 +00:00
|
|
|
|
|
2020-12-03 23:15:59 +00:00
|
|
|
|
// Index of the section we're currently looking at
|
2020-12-08 23:38:10 +00:00
|
|
|
|
sectionIndex int
|
2020-11-28 02:52:51 +00:00
|
|
|
|
|
|
|
|
|
// Tracks what exactly is loaded between the stash, news and local files
|
|
|
|
|
loaded DocTypeSet
|
2020-05-15 00:23:11 +00:00
|
|
|
|
|
2020-11-23 21:56:44 +00:00
|
|
|
|
// The master set of markdown documents we're working with.
|
|
|
|
|
markdowns []*markdown
|
|
|
|
|
|
|
|
|
|
// Markdown documents we're currently displaying. Filtering, toggles and so
|
|
|
|
|
// on will alter this slice so we can show what is relevant. For that
|
|
|
|
|
// reason, this field should be considered ephemeral.
|
|
|
|
|
filteredMarkdowns []*markdown
|
|
|
|
|
|
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-12-08 23:38:10 +00:00
|
|
|
|
serverPage int
|
2020-05-15 00:23:11 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-10-18 01:46:02 +00:00
|
|
|
|
func (m stashModel) localOnly() bool {
|
2020-12-11 01:43:31 +00:00
|
|
|
|
return m.common.cfg.localOnly()
|
2020-10-18 01:46:02 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (m stashModel) stashedOnly() bool {
|
2020-12-11 01:43:31 +00:00
|
|
|
|
return m.common.cfg.stashedOnly()
|
2020-10-18 01:46:02 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-10-18 03:01:20 +00:00
|
|
|
|
func (m stashModel) loadingDone() bool {
|
2020-12-11 01:43:31 +00:00
|
|
|
|
return m.loaded.Equals(m.common.cfg.DocumentTypes.Difference(ConvertedDoc))
|
2020-10-18 03:01:20 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-12-08 23:38:10 +00:00
|
|
|
|
func (m stashModel) hasSection(key sectionKey) bool {
|
2020-12-03 23:15:59 +00:00
|
|
|
|
for _, v := range m.sections {
|
2020-12-08 23:38:10 +00:00
|
|
|
|
if key == v.key {
|
2020-12-03 23:15:59 +00:00
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2020-12-08 23:38:10 +00:00
|
|
|
|
func (m stashModel) currentSection() *section {
|
|
|
|
|
return &m.sections[m.sectionIndex]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (m stashModel) paginator() *paginator.Model {
|
|
|
|
|
return &m.currentSection().paginator
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (m *stashModel) setPaginator(p paginator.Model) {
|
|
|
|
|
m.currentSection().paginator = p
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (m stashModel) cursor() int {
|
|
|
|
|
return m.currentSection().cursor
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (m *stashModel) setCursor(i int) {
|
|
|
|
|
m.currentSection().cursor = i
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-26 03:08:24 +00:00
|
|
|
|
// Returns whether or not we're online. That is, when "local-only" mode is
|
|
|
|
|
// disabled and we've authenticated successfully.
|
|
|
|
|
func (m stashModel) online() bool {
|
2020-12-11 01:43:31 +00:00
|
|
|
|
return !m.localOnly() && m.common.authStatus == authOK
|
2020-11-26 03:08:24 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-05-19 17:20:39 +00:00
|
|
|
|
func (m *stashModel) setSize(width, height int) {
|
2020-12-11 01:43:31 +00:00
|
|
|
|
m.common.width = width
|
|
|
|
|
m.common.height = height
|
2020-05-15 00:23:11 +00:00
|
|
|
|
|
2020-11-17 22:37:52 +00:00
|
|
|
|
m.noteInput.Width = width - stashViewHorizontalPadding*2 - ansi.PrintableRuneWidth(m.noteInput.Prompt)
|
|
|
|
|
m.filterInput.Width = width - stashViewHorizontalPadding*2 - ansi.PrintableRuneWidth(m.filterInput.Prompt)
|
2020-12-09 20:24:24 +00:00
|
|
|
|
|
|
|
|
|
m.updatePagination()
|
2020-10-24 20:47:40 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-11-23 21:56:44 +00:00
|
|
|
|
func (m *stashModel) resetFiltering() {
|
2020-11-26 01:15:21 +00:00
|
|
|
|
m.filterState = unfiltered
|
2020-11-23 21:56:44 +00:00
|
|
|
|
m.filterInput.Reset()
|
|
|
|
|
sort.Stable(markdownsByLocalFirst(m.markdowns))
|
|
|
|
|
m.filteredMarkdowns = nil
|
2020-12-08 23:38:10 +00:00
|
|
|
|
m.updatePagination()
|
2020-11-23 21:56:44 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Is a filter currently being applied?
|
|
|
|
|
func (m stashModel) isFiltering() bool {
|
2020-11-26 01:15:21 +00:00
|
|
|
|
return m.filterState != unfiltered
|
2020-11-23 21:56:44 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Should we be updating the filter?
|
|
|
|
|
func (m stashModel) shouldUpdateFilter() bool {
|
|
|
|
|
// If we're in the middle of setting a note don't update the filter so that
|
|
|
|
|
// the focus won't jump around.
|
2020-11-26 03:49:49 +00:00
|
|
|
|
return m.isFiltering() && m.selectionState != selectionSettingNote
|
2020-11-23 21:56:44 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-12-08 23:38:10 +00:00
|
|
|
|
// Update pagination according to the amount of markdowns for the current
|
|
|
|
|
// state.
|
|
|
|
|
func (m *stashModel) updatePagination() {
|
2020-11-30 17:17:30 +00:00
|
|
|
|
_, helpHeight := m.helpView()
|
|
|
|
|
|
2020-12-11 01:43:31 +00:00
|
|
|
|
availableHeight := m.common.height -
|
2020-11-30 17:17:30 +00:00
|
|
|
|
stashViewTopPadding -
|
2020-11-30 23:44:58 +00:00
|
|
|
|
helpHeight -
|
|
|
|
|
stashViewBottomPadding
|
|
|
|
|
|
2020-12-08 23:38:10 +00:00
|
|
|
|
m.paginator().PerPage = max(1, availableHeight/stashViewItemHeight)
|
2020-10-28 01:17:18 +00:00
|
|
|
|
|
2020-11-23 21:56:44 +00:00
|
|
|
|
if pages := len(m.getVisibleMarkdowns()); pages < 1 {
|
2020-12-08 23:38:10 +00:00
|
|
|
|
m.paginator().SetTotalPages(1)
|
2020-10-28 01:17:18 +00:00
|
|
|
|
} else {
|
2020-12-08 23:38:10 +00:00
|
|
|
|
m.paginator().SetTotalPages(pages)
|
2020-10-28 01:17:18 +00:00
|
|
|
|
}
|
2020-05-22 02:29:46 +00:00
|
|
|
|
|
2020-05-15 02:48:38 +00:00
|
|
|
|
// Make sure the page stays in bounds
|
2020-12-08 23:38:10 +00:00
|
|
|
|
if m.paginator().Page >= m.paginator().TotalPages-1 {
|
|
|
|
|
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-08-24 20:07:43 +00:00
|
|
|
|
// MarkdownIndex returns the index of the currently selected markdown item.
|
2020-05-21 19:14:33 +00:00
|
|
|
|
func (m stashModel) markdownIndex() int {
|
2020-12-08 23:38:10 +00:00
|
|
|
|
return m.paginator().Page*m.paginator().PerPage + m.cursor()
|
2020-05-21 19:14:33 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-08-24 20:07:43 +00:00
|
|
|
|
// Return the current selected markdown in the stash.
|
2020-05-22 20:28:15 +00:00
|
|
|
|
func (m stashModel) selectedMarkdown() *markdown {
|
2020-07-15 19:51:51 +00:00
|
|
|
|
i := m.markdownIndex()
|
2020-10-24 20:45:05 +00:00
|
|
|
|
|
2020-11-23 21:56:44 +00:00
|
|
|
|
mds := m.getVisibleMarkdowns()
|
|
|
|
|
if i < 0 || len(mds) == 0 || len(mds) <= i {
|
2020-06-02 22:58:15 +00:00
|
|
|
|
return nil
|
|
|
|
|
}
|
2020-10-24 20:45:05 +00:00
|
|
|
|
|
2020-11-23 21:56:44 +00:00
|
|
|
|
return mds[i]
|
2020-05-22 20:28:15 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-08-24 20:07:43 +00:00
|
|
|
|
// Adds markdown documents to the model.
|
2020-05-22 20:01:23 +00:00
|
|
|
|
func (m *stashModel) addMarkdowns(mds ...*markdown) {
|
2020-07-15 20:03:05 +00:00
|
|
|
|
if len(mds) > 0 {
|
2020-12-10 01:12:01 +00:00
|
|
|
|
for _, md := range mds {
|
|
|
|
|
md.generateLocalID()
|
|
|
|
|
}
|
|
|
|
|
|
2020-07-15 20:03:05 +00:00
|
|
|
|
m.markdowns = append(m.markdowns, mds...)
|
2020-11-23 21:56:44 +00:00
|
|
|
|
if !m.isFiltering() {
|
|
|
|
|
sort.Stable(markdownsByLocalFirst(m.markdowns))
|
|
|
|
|
}
|
2020-12-08 23:38:10 +00:00
|
|
|
|
m.updatePagination()
|
2020-07-15 20:03:05 +00:00
|
|
|
|
}
|
2020-05-22 20:01:23 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-08-24 20:07:43 +00:00
|
|
|
|
// Return the number of markdown documents of a given type.
|
2020-11-28 02:52:51 +00:00
|
|
|
|
func (m stashModel) countMarkdowns(t DocType) (found int) {
|
2020-12-03 23:15:59 +00:00
|
|
|
|
if len(m.markdowns) == 0 {
|
2020-07-15 23:56:37 +00:00
|
|
|
|
return
|
|
|
|
|
}
|
2020-12-09 20:24:24 +00:00
|
|
|
|
|
|
|
|
|
var mds []*markdown
|
|
|
|
|
if m.isFiltering() {
|
|
|
|
|
mds = m.getVisibleMarkdowns()
|
|
|
|
|
} else {
|
|
|
|
|
mds = m.markdowns
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for i := 0; i < len(mds); i++ {
|
|
|
|
|
if mds[i].markdownType == t {
|
2020-07-15 23:56:37 +00:00
|
|
|
|
found++
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-25 16:40:24 +00:00
|
|
|
|
// Sift through the master markdown collection for the specified types.
|
2020-11-28 02:52:51 +00:00
|
|
|
|
func (m stashModel) getMarkdownByType(types ...DocType) []*markdown {
|
2020-11-25 16:40:24 +00:00
|
|
|
|
var agg []*markdown
|
|
|
|
|
|
|
|
|
|
if len(m.markdowns) == 0 {
|
|
|
|
|
return agg
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, t := range types {
|
2020-11-19 23:56:46 +00:00
|
|
|
|
for _, md := range m.markdowns {
|
2020-11-25 16:40:24 +00:00
|
|
|
|
if md.markdownType == t {
|
|
|
|
|
agg = append(agg, md)
|
2020-11-19 23:56:46 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-11-25 16:40:12 +00:00
|
|
|
|
|
2020-11-25 16:40:24 +00:00
|
|
|
|
sort.Sort(markdownsByLocalFirst(agg))
|
|
|
|
|
return agg
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Returns the markdowns that should be currently shown.
|
|
|
|
|
func (m stashModel) getVisibleMarkdowns() []*markdown {
|
2020-11-23 21:56:44 +00:00
|
|
|
|
if m.isFiltering() {
|
|
|
|
|
return m.filteredMarkdowns
|
|
|
|
|
}
|
2020-11-25 16:40:24 +00:00
|
|
|
|
|
2020-12-08 23:38:10 +00:00
|
|
|
|
return m.getMarkdownByType(m.currentSection().docTypes.AsSlice()...)
|
2020-11-25 16:40:24 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Return the markdowns eligible to be filtered.
|
|
|
|
|
func (m stashModel) getFilterableMarkdowns() []*markdown {
|
2020-12-09 20:24:24 +00:00
|
|
|
|
return m.getMarkdownByType(LocalDoc, ConvertedDoc, StashedDoc)
|
2020-11-23 21:56:44 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-11-18 01:46:26 +00:00
|
|
|
|
// Command for opening a markdown document in the pager. Note that this also
|
|
|
|
|
// alters the model.
|
|
|
|
|
func (m *stashModel) openMarkdown(md *markdown) tea.Cmd {
|
|
|
|
|
var cmd tea.Cmd
|
2020-11-28 23:30:51 +00:00
|
|
|
|
m.viewState = stashStateLoadingDocument
|
2020-11-18 01:46:26 +00:00
|
|
|
|
|
2020-11-28 02:52:51 +00:00
|
|
|
|
if md.markdownType == LocalDoc {
|
2020-11-18 01:46:26 +00:00
|
|
|
|
cmd = loadLocalMarkdown(md)
|
|
|
|
|
} else {
|
2020-12-11 01:43:31 +00:00
|
|
|
|
cmd = loadRemoteMarkdown(m.common.cc, md)
|
2020-11-18 01:46:26 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return tea.Batch(cmd, spinner.Tick)
|
|
|
|
|
}
|
|
|
|
|
|
2020-12-14 21:26:29 +00:00
|
|
|
|
func (m *stashModel) newStatusMessage(sm statusMessage) tea.Cmd {
|
2020-12-09 20:24:24 +00:00
|
|
|
|
m.showStatusMessage = true
|
2020-12-14 21:26:29 +00:00
|
|
|
|
m.statusMessage = sm
|
2020-12-09 20:24:24 +00:00
|
|
|
|
if m.statusMessageTimer != nil {
|
|
|
|
|
m.statusMessageTimer.Stop()
|
|
|
|
|
}
|
|
|
|
|
m.statusMessageTimer = time.NewTimer(statusMessageTimeout)
|
|
|
|
|
return waitForStatusMessageTimeout(stashContext, m.statusMessageTimer)
|
|
|
|
|
}
|
|
|
|
|
|
2020-08-21 17:39:59 +00:00
|
|
|
|
func (m *stashModel) hideStatusMessage() {
|
|
|
|
|
m.showStatusMessage = false
|
2020-12-14 21:26:29 +00:00
|
|
|
|
m.statusMessage = statusMessage{}
|
2020-08-21 17:39:59 +00:00
|
|
|
|
if m.statusMessageTimer != nil {
|
|
|
|
|
m.statusMessageTimer.Stop()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-10-24 20:50:34 +00:00
|
|
|
|
func (m *stashModel) moveCursorUp() {
|
2020-12-08 23:38:10 +00:00
|
|
|
|
m.setCursor(m.cursor() - 1)
|
|
|
|
|
if m.cursor() < 0 && m.paginator().Page == 0 {
|
2020-10-24 20:50:34 +00:00
|
|
|
|
// Stop
|
2020-12-08 23:38:10 +00:00
|
|
|
|
m.setCursor(0)
|
2020-10-24 20:50:34 +00:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2020-12-08 23:38:10 +00:00
|
|
|
|
if m.cursor() >= 0 {
|
2020-10-24 20:50:34 +00:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
// Go to previous page
|
2020-12-08 23:38:10 +00:00
|
|
|
|
m.paginator().PrevPage()
|
2020-10-24 20:50:34 +00:00
|
|
|
|
|
2020-12-08 23:38:10 +00:00
|
|
|
|
m.setCursor(m.paginator().ItemsOnPage(len(m.getVisibleMarkdowns())) - 1)
|
2020-10-24 20:50:34 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (m *stashModel) moveCursorDown() {
|
2020-12-08 23:38:10 +00:00
|
|
|
|
itemsOnPage := m.paginator().ItemsOnPage(len(m.getVisibleMarkdowns()))
|
2020-10-24 20:50:34 +00:00
|
|
|
|
|
2020-12-08 23:38:10 +00:00
|
|
|
|
m.setCursor(m.cursor() + 1)
|
|
|
|
|
if m.cursor() < itemsOnPage {
|
2020-10-24 20:50:34 +00:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2020-12-08 23:38:10 +00:00
|
|
|
|
if !m.paginator().OnLastPage() {
|
|
|
|
|
m.paginator().NextPage()
|
|
|
|
|
m.setCursor(0)
|
2020-10-24 20:50:34 +00:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-18 01:46:26 +00:00
|
|
|
|
// During filtering the cursor position can exceed the number of
|
2020-10-24 20:50:34 +00:00
|
|
|
|
// itemsOnPage. It's more intuitive to start the cursor at the
|
|
|
|
|
// topmost position when moving it down in this scenario.
|
2020-12-08 23:38:10 +00:00
|
|
|
|
if m.cursor() > itemsOnPage {
|
|
|
|
|
m.setCursor(0)
|
2020-10-24 20:50:34 +00:00
|
|
|
|
return
|
|
|
|
|
}
|
2020-12-08 23:38:10 +00:00
|
|
|
|
m.setCursor(itemsOnPage - 1)
|
2020-10-24 20:50:34 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-05-13 16:53:58 +00:00
|
|
|
|
// INIT
|
|
|
|
|
|
2020-12-11 01:43:31 +00:00
|
|
|
|
func newStashModel(common *commonModel) stashModel {
|
2020-08-25 13:40:53 +00:00
|
|
|
|
sp := spinner.NewModel()
|
2020-11-13 21:17:50 +00:00
|
|
|
|
sp.Spinner = spinner.Line
|
2020-12-11 01:43:31 +00:00
|
|
|
|
sp.ForegroundColor = lib.SpinnerColor.String()
|
2020-12-14 20:44:03 +00:00
|
|
|
|
sp.HideFor = time.Millisecond * 100
|
|
|
|
|
sp.MinimumLifetime = time.Millisecond * 180
|
2020-08-25 13:40:53 +00:00
|
|
|
|
sp.Start()
|
2020-05-13 16:53:58 +00:00
|
|
|
|
|
2020-05-22 02:29:46 +00:00
|
|
|
|
ni := textinput.NewModel()
|
2020-11-11 22:56:40 +00:00
|
|
|
|
ni.Prompt = stashTextInputPromptStyle("Memo: ")
|
2020-12-11 01:43:31 +00:00
|
|
|
|
ni.CursorColor = lib.Fuschia.String()
|
2020-05-22 22:42:18 +00:00
|
|
|
|
ni.CharLimit = noteCharacterLimit
|
2020-05-22 02:29:46 +00:00
|
|
|
|
ni.Focus()
|
|
|
|
|
|
2020-10-24 20:37:18 +00:00
|
|
|
|
si := textinput.NewModel()
|
2020-11-13 21:33:22 +00:00
|
|
|
|
si.Prompt = stashTextInputPromptStyle("Filter: ")
|
2020-12-11 01:43:31 +00:00
|
|
|
|
si.CursorColor = lib.Fuschia.String()
|
2020-10-24 20:37:18 +00:00
|
|
|
|
si.CharLimit = noteCharacterLimit
|
|
|
|
|
si.Focus()
|
|
|
|
|
|
2020-12-08 23:38:10 +00:00
|
|
|
|
var s []section
|
2020-12-11 01:43:31 +00:00
|
|
|
|
if common.cfg.localOnly() {
|
2020-12-08 23:38:10 +00:00
|
|
|
|
s = []section{
|
|
|
|
|
sections[localSection],
|
|
|
|
|
}
|
2020-12-11 01:43:31 +00:00
|
|
|
|
} else if common.cfg.stashedOnly() {
|
2020-12-08 23:38:10 +00:00
|
|
|
|
s = []section{
|
|
|
|
|
sections[stashedSection],
|
|
|
|
|
sections[newsSection],
|
|
|
|
|
}
|
2020-12-03 23:15:59 +00:00
|
|
|
|
} else {
|
2020-12-08 23:38:10 +00:00
|
|
|
|
s = []section{
|
|
|
|
|
sections[localSection],
|
|
|
|
|
sections[stashedSection],
|
|
|
|
|
sections[newsSection],
|
2020-12-03 23:15:59 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-12-08 23:38:10 +00:00
|
|
|
|
p := paginator.NewModel()
|
|
|
|
|
p.Type = paginator.Dots
|
|
|
|
|
p.ActiveDot = brightGrayFg("•")
|
|
|
|
|
p.InactiveDot = darkGrayFg("•")
|
|
|
|
|
|
|
|
|
|
for i := range s {
|
|
|
|
|
s[i].paginator = p
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-13 16:53:58 +00:00
|
|
|
|
m := stashModel{
|
2020-12-11 01:43:31 +00:00
|
|
|
|
common: common,
|
2020-12-11 01:20:43 +00:00
|
|
|
|
spinner: sp,
|
|
|
|
|
noteInput: ni,
|
|
|
|
|
filterInput: si,
|
|
|
|
|
serverPage: 1,
|
|
|
|
|
loaded: NewDocTypeSet(),
|
|
|
|
|
sections: s,
|
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-11-25 16:40:24 +00:00
|
|
|
|
func (m stashModel) update(msg tea.Msg) (stashModel, tea.Cmd) {
|
2020-07-15 23:19:44 +00:00
|
|
|
|
var 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-16 19:19:59 +00:00
|
|
|
|
case errMsg:
|
|
|
|
|
m.err = msg
|
|
|
|
|
|
2020-07-17 16:25:40 +00:00
|
|
|
|
case stashLoadErrMsg:
|
|
|
|
|
m.err = msg.err
|
2020-11-28 02:52:51 +00:00
|
|
|
|
m.loaded.Add(StashedDoc) // still done, albeit unsuccessfully
|
2020-07-17 16:25:40 +00:00
|
|
|
|
m.stashFullyLoaded = true
|
|
|
|
|
|
|
|
|
|
case newsLoadErrMsg:
|
|
|
|
|
m.err = msg.err
|
2020-11-28 02:52:51 +00:00
|
|
|
|
m.loaded.Add(NewsDoc) // still done, albeit unsuccessfully
|
2020-07-17 16:25:40 +00:00
|
|
|
|
|
2020-07-15 20:41:41 +00:00
|
|
|
|
case localFileSearchFinished:
|
2020-11-20 03:26:31 +00:00
|
|
|
|
// We're finished searching for local files
|
2020-11-28 02:52:51 +00:00
|
|
|
|
m.loaded.Add(LocalDoc)
|
2020-07-15 20:41:41 +00:00
|
|
|
|
|
2020-11-20 03:45:02 +00:00
|
|
|
|
case gotStashMsg, gotNewsMsg:
|
|
|
|
|
// Stash or news results have come in from the server.
|
2020-11-20 03:26:31 +00:00
|
|
|
|
//
|
2020-11-20 03:45:02 +00:00
|
|
|
|
// With the stash, this doesn't mean the whole stash listing is loaded,
|
2020-11-26 01:15:21 +00:00
|
|
|
|
// but now know it can load, at least, so mark the stash as loaded here.
|
2020-11-20 03:45:02 +00:00
|
|
|
|
var docs []*markdown
|
|
|
|
|
|
|
|
|
|
switch msg := msg.(type) {
|
|
|
|
|
case gotStashMsg:
|
2020-11-28 02:52:51 +00:00
|
|
|
|
m.loaded.Add(StashedDoc)
|
|
|
|
|
docs = wrapMarkdowns(StashedDoc, msg)
|
2020-05-15 23:16:22 +00:00
|
|
|
|
|
2020-11-20 03:45:02 +00:00
|
|
|
|
if len(msg) == 0 {
|
2020-11-20 17:57:03 +00:00
|
|
|
|
// If the server comes back with nothing then we've got
|
|
|
|
|
// everything
|
2020-11-20 03:45:02 +00:00
|
|
|
|
m.stashFullyLoaded = true
|
2020-11-20 17:57:03 +00:00
|
|
|
|
} else {
|
|
|
|
|
// Load the next page
|
2020-12-08 23:38:10 +00:00
|
|
|
|
m.serverPage++
|
2020-11-20 17:57:03 +00:00
|
|
|
|
cmds = append(cmds, loadStash(m))
|
2020-11-20 03:26:31 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-11-20 03:45:02 +00:00
|
|
|
|
case gotNewsMsg:
|
2020-11-28 02:52:51 +00:00
|
|
|
|
m.loaded.Add(NewsDoc)
|
|
|
|
|
docs = wrapMarkdowns(NewsDoc, msg)
|
2020-05-15 23:16:22 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-11-20 03:45:02 +00:00
|
|
|
|
// If we're filtering build filter indexes immediately so any
|
|
|
|
|
// matching results will show up in the filter.
|
2020-11-23 21:56:44 +00:00
|
|
|
|
if m.isFiltering() {
|
2020-11-20 03:45:02 +00:00
|
|
|
|
for _, md := range docs {
|
|
|
|
|
md.buildFilterValue()
|
2020-11-20 03:26:31 +00:00
|
|
|
|
}
|
2020-05-21 19:14:33 +00:00
|
|
|
|
}
|
2020-11-23 21:56:44 +00:00
|
|
|
|
if m.shouldUpdateFilter() {
|
|
|
|
|
cmds = append(cmds, filterMarkdowns(m))
|
|
|
|
|
}
|
2020-05-21 19:14:33 +00:00
|
|
|
|
|
2020-11-20 03:45:02 +00:00
|
|
|
|
m.addMarkdowns(docs...)
|
|
|
|
|
|
2020-12-10 01:12:01 +00:00
|
|
|
|
case markdownFetchFailedMsg:
|
|
|
|
|
s := "Couldn't load markdown"
|
|
|
|
|
if msg.note != "" {
|
|
|
|
|
s += ": " + msg.note
|
|
|
|
|
}
|
2020-12-14 21:26:29 +00:00
|
|
|
|
cmd := m.newStatusMessage(statusMessage{
|
|
|
|
|
status: normalStatusMessage,
|
|
|
|
|
message: s,
|
|
|
|
|
})
|
2020-12-10 01:12:01 +00:00
|
|
|
|
return m, cmd
|
|
|
|
|
|
2020-11-23 21:56:44 +00:00
|
|
|
|
case filteredMarkdownMsg:
|
|
|
|
|
m.filteredMarkdowns = msg
|
|
|
|
|
return m, nil
|
|
|
|
|
|
2020-06-22 19:11:48 +00:00
|
|
|
|
case spinner.TickMsg:
|
2020-12-11 01:20:43 +00:00
|
|
|
|
loading := !m.loadingDone()
|
2020-12-11 01:43:31 +00:00
|
|
|
|
stashing := m.common.isStashing()
|
2020-12-11 01:20:43 +00:00
|
|
|
|
openingDocument := m.viewState == stashStateLoadingDocument
|
|
|
|
|
spinnerVisible := m.spinner.Visible()
|
2020-08-21 21:30:33 +00:00
|
|
|
|
|
2020-12-11 01:20:43 +00:00
|
|
|
|
if loading || stashing || openingDocument || spinnerVisible {
|
2020-11-13 21:17:50 +00:00
|
|
|
|
newSpinnerModel, cmd := m.spinner.Update(msg)
|
2020-07-15 23:19:44 +00:00
|
|
|
|
m.spinner = newSpinnerModel
|
2020-05-15 22:39:23 +00:00
|
|
|
|
cmds = append(cmds, cmd)
|
2020-12-12 00:54:19 +00:00
|
|
|
|
} else if !stashing && !spinnerVisible && m.stashStatusMessageQueued {
|
|
|
|
|
m.stashStatusMessageQueued = false
|
|
|
|
|
cmds = append(cmds, m.newStatusMessage(stashedStatusMessage))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if spinnerVisible && m.showStatusMessage {
|
|
|
|
|
m.hideStatusMessage()
|
2020-05-13 16:53:58 +00:00
|
|
|
|
}
|
2020-05-20 19:18:59 +00:00
|
|
|
|
|
2020-07-15 19:51:51 +00:00
|
|
|
|
// A note was set on a document. This may have happened in the pager so
|
2020-05-22 02:29:46 +00:00
|
|
|
|
// 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-08-21 17:39:59 +00:00
|
|
|
|
|
2020-12-11 01:20:43 +00:00
|
|
|
|
// Note: mechanical stuff related to stash success is handled in the parent
|
|
|
|
|
// update function.
|
2020-08-21 17:39:59 +00:00
|
|
|
|
case stashSuccessMsg:
|
2020-12-12 00:54:19 +00:00
|
|
|
|
m.spinner.Finish()
|
|
|
|
|
if m.spinner.Visible() {
|
|
|
|
|
// We want to show the 'stashed!' status message, but need to wait
|
|
|
|
|
// until the spinner goes away first.
|
|
|
|
|
m.stashStatusMessageQueued = true
|
|
|
|
|
} else {
|
|
|
|
|
cmds = append(cmds, m.newStatusMessage(stashedStatusMessage))
|
|
|
|
|
}
|
2020-08-21 17:39:59 +00:00
|
|
|
|
|
2020-12-11 01:20:43 +00:00
|
|
|
|
// Note: mechanical stuff related to stash failure is handled in the parent
|
|
|
|
|
// update function.
|
2020-12-11 00:26:24 +00:00
|
|
|
|
case stashFailMsg:
|
2020-12-14 21:26:29 +00:00
|
|
|
|
cmds = append(cmds, m.newStatusMessage(statusMessage{
|
|
|
|
|
status: normalStatusMessage,
|
|
|
|
|
message: "Couldn’t stash :(",
|
|
|
|
|
}))
|
2020-12-11 00:26:24 +00:00
|
|
|
|
|
2020-08-21 17:39:59 +00:00
|
|
|
|
case statusMessageTimeoutMsg:
|
|
|
|
|
if applicationContext(msg) == stashContext {
|
|
|
|
|
m.hideStatusMessage()
|
|
|
|
|
}
|
2020-05-13 16:53:58 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-11-26 01:15:21 +00:00
|
|
|
|
if m.filterState == filtering {
|
|
|
|
|
cmds = append(cmds, m.handleFiltering(msg))
|
|
|
|
|
return m, tea.Batch(cmds...)
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-26 03:49:49 +00:00
|
|
|
|
switch m.selectionState {
|
|
|
|
|
case selectionSettingNote:
|
|
|
|
|
cmds = append(cmds, m.handleNoteInput(msg))
|
|
|
|
|
return m, tea.Batch(cmds...)
|
|
|
|
|
case selectionPromptingDelete:
|
|
|
|
|
cmds = append(cmds, m.handleDeleteConfirmation(msg))
|
|
|
|
|
return m, tea.Batch(cmds...)
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-25 16:40:24 +00:00
|
|
|
|
// Updates per the current state
|
2020-11-28 23:30:51 +00:00
|
|
|
|
switch m.viewState {
|
|
|
|
|
case stashStateReady:
|
2020-11-25 16:40:24 +00:00
|
|
|
|
cmds = append(cmds, m.handleDocumentBrowsing(msg))
|
|
|
|
|
case stashStateShowingError:
|
|
|
|
|
// Any key exists the error view
|
|
|
|
|
if _, ok := msg.(tea.KeyMsg); ok {
|
2020-11-28 23:30:51 +00:00
|
|
|
|
m.viewState = stashStateReady
|
2020-11-25 16:40:24 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
2020-05-22 13:38:34 +00:00
|
|
|
|
|
2020-11-25 16:40:24 +00:00
|
|
|
|
return m, tea.Batch(cmds...)
|
|
|
|
|
}
|
2020-08-25 23:32:19 +00:00
|
|
|
|
|
2020-12-01 03:12:05 +00:00
|
|
|
|
// Updates for when a user is browsing the markdown listing.
|
2020-11-25 16:40:24 +00:00
|
|
|
|
func (m *stashModel) handleDocumentBrowsing(msg tea.Msg) tea.Cmd {
|
|
|
|
|
var cmds []tea.Cmd
|
2020-08-25 23:32:19 +00:00
|
|
|
|
|
2020-12-09 20:24:24 +00:00
|
|
|
|
numDocs := len(m.getVisibleMarkdowns())
|
2020-10-24 20:47:40 +00:00
|
|
|
|
|
2020-11-25 16:40:24 +00:00
|
|
|
|
switch msg := msg.(type) {
|
|
|
|
|
// Handle keys
|
|
|
|
|
case tea.KeyMsg:
|
|
|
|
|
switch msg.String() {
|
2020-12-03 23:15:59 +00:00
|
|
|
|
case "k", "ctrl+k", "up":
|
2020-11-25 16:40:24 +00:00
|
|
|
|
m.moveCursorUp()
|
|
|
|
|
|
2020-12-03 23:15:59 +00:00
|
|
|
|
case "j", "ctrl+j", "down":
|
2020-11-25 16:40:24 +00:00
|
|
|
|
m.moveCursorDown()
|
|
|
|
|
|
|
|
|
|
// Go to the very start
|
|
|
|
|
case "home", "g":
|
2020-12-08 23:38:10 +00:00
|
|
|
|
m.paginator().Page = 0
|
|
|
|
|
m.setCursor(0)
|
2020-11-25 16:40:24 +00:00
|
|
|
|
|
|
|
|
|
// Go to the very end
|
|
|
|
|
case "end", "G":
|
2020-12-08 23:38:10 +00:00
|
|
|
|
m.paginator().Page = m.paginator().TotalPages - 1
|
2020-12-09 20:24:24 +00:00
|
|
|
|
m.setCursor(m.paginator().ItemsOnPage(numDocs) - 1)
|
2020-11-25 16:40:24 +00:00
|
|
|
|
|
|
|
|
|
case "esc":
|
2020-11-28 23:30:51 +00:00
|
|
|
|
if m.isFiltering() {
|
|
|
|
|
m.resetFiltering()
|
|
|
|
|
break
|
|
|
|
|
}
|
2020-12-03 23:15:59 +00:00
|
|
|
|
|
|
|
|
|
case "tab":
|
2020-12-09 20:24:24 +00:00
|
|
|
|
if len(m.sections) == 0 || m.isFiltering() {
|
2020-12-03 23:15:59 +00:00
|
|
|
|
break
|
|
|
|
|
}
|
2020-12-08 23:38:10 +00:00
|
|
|
|
m.sectionIndex++
|
|
|
|
|
if m.sectionIndex >= len(m.sections) {
|
|
|
|
|
m.sectionIndex = 0
|
2020-12-03 23:15:59 +00:00
|
|
|
|
}
|
2020-12-08 23:38:10 +00:00
|
|
|
|
m.updatePagination()
|
2020-12-03 23:15:59 +00:00
|
|
|
|
|
|
|
|
|
case "shift+tab":
|
|
|
|
|
if len(m.sections) == 0 {
|
|
|
|
|
break
|
|
|
|
|
}
|
2020-12-08 23:38:10 +00:00
|
|
|
|
m.sectionIndex--
|
|
|
|
|
if m.sectionIndex < 0 {
|
|
|
|
|
m.sectionIndex = len(m.sections) - 1
|
2020-12-03 23:15:59 +00:00
|
|
|
|
}
|
2020-12-08 23:38:10 +00:00
|
|
|
|
m.updatePagination()
|
2020-08-21 17:39:59 +00:00
|
|
|
|
|
2020-11-25 16:40:24 +00:00
|
|
|
|
// Open document
|
|
|
|
|
case "enter":
|
|
|
|
|
m.hideStatusMessage()
|
2020-06-03 16:46:53 +00:00
|
|
|
|
|
2020-12-09 20:24:24 +00:00
|
|
|
|
if numDocs == 0 {
|
2020-11-25 16:40:24 +00:00
|
|
|
|
break
|
|
|
|
|
}
|
2020-05-22 13:38:34 +00:00
|
|
|
|
|
2020-11-25 16:40:24 +00:00
|
|
|
|
// Load the document from the server. We'll handle the message
|
|
|
|
|
// that comes back in the main update function.
|
|
|
|
|
md := m.selectedMarkdown()
|
|
|
|
|
cmds = append(cmds, m.openMarkdown(md))
|
2020-11-19 23:56:46 +00:00
|
|
|
|
|
2020-11-25 16:40:24 +00:00
|
|
|
|
// Filter your notes
|
|
|
|
|
case "/":
|
|
|
|
|
m.hideStatusMessage()
|
2020-10-24 20:18:19 +00:00
|
|
|
|
|
2020-11-25 16:40:24 +00:00
|
|
|
|
// Build values we'll filter against
|
|
|
|
|
for _, md := range m.markdowns {
|
|
|
|
|
md.buildFilterValue()
|
|
|
|
|
}
|
2020-11-17 15:27:57 +00:00
|
|
|
|
|
2020-11-25 16:40:24 +00:00
|
|
|
|
m.filteredMarkdowns = m.getFilterableMarkdowns()
|
2020-11-23 21:56:44 +00:00
|
|
|
|
|
2020-12-08 23:38:10 +00:00
|
|
|
|
m.paginator().Page = 0
|
|
|
|
|
m.setCursor(0)
|
2020-11-26 01:15:21 +00:00
|
|
|
|
m.filterState = filtering
|
2020-11-25 16:40:24 +00:00
|
|
|
|
m.filterInput.CursorEnd()
|
|
|
|
|
m.filterInput.Focus()
|
|
|
|
|
return textinput.Blink
|
2020-10-24 20:37:18 +00:00
|
|
|
|
|
2020-11-25 16:40:24 +00:00
|
|
|
|
// Set note
|
|
|
|
|
case "m":
|
|
|
|
|
m.hideStatusMessage()
|
2020-08-21 17:39:59 +00:00
|
|
|
|
|
2020-12-09 20:24:24 +00:00
|
|
|
|
if numDocs == 0 {
|
2020-11-25 16:40:24 +00:00
|
|
|
|
break
|
|
|
|
|
}
|
2020-10-28 00:48:07 +00:00
|
|
|
|
|
2020-11-25 16:40:24 +00:00
|
|
|
|
md := m.selectedMarkdown()
|
2020-11-28 02:52:51 +00:00
|
|
|
|
isUserMarkdown := md.markdownType == StashedDoc || md.markdownType == ConvertedDoc
|
2020-11-26 03:49:49 +00:00
|
|
|
|
isSettingNote := m.selectionState == selectionSettingNote
|
|
|
|
|
isPromptingDelete := m.selectionState == selectionPromptingDelete
|
2020-05-22 20:28:15 +00:00
|
|
|
|
|
2020-11-25 16:40:24 +00:00
|
|
|
|
if isUserMarkdown && !isSettingNote && !isPromptingDelete {
|
2020-11-26 03:49:49 +00:00
|
|
|
|
m.selectionState = selectionSettingNote
|
2020-11-25 16:40:24 +00:00
|
|
|
|
m.noteInput.SetValue(md.Note)
|
|
|
|
|
m.noteInput.CursorEnd()
|
|
|
|
|
return textinput.Blink
|
|
|
|
|
}
|
2020-05-22 13:38:34 +00:00
|
|
|
|
|
2020-11-25 16:40:24 +00:00
|
|
|
|
// Stash
|
|
|
|
|
case "s":
|
2020-12-09 20:24:24 +00:00
|
|
|
|
if numDocs == 0 || !m.online() || m.selectedMarkdown() == nil {
|
2020-11-25 16:40:24 +00:00
|
|
|
|
break
|
|
|
|
|
}
|
2020-10-28 00:48:07 +00:00
|
|
|
|
|
2020-11-25 16:40:24 +00:00
|
|
|
|
md := m.selectedMarkdown()
|
2020-09-10 17:32:57 +00:00
|
|
|
|
|
2020-12-11 00:26:24 +00:00
|
|
|
|
if !stashableDocTypes.Contains(md.markdownType) {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
|
2020-12-11 01:43:31 +00:00
|
|
|
|
if _, alreadyStashed := m.common.filesStashed[md.localID]; alreadyStashed {
|
2020-12-14 21:26:29 +00:00
|
|
|
|
cmds = append(cmds, m.newStatusMessage(alreadyStashedStatusMessage))
|
2020-12-09 20:24:24 +00:00
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
|
2020-12-10 01:12:01 +00:00
|
|
|
|
if !stashableDocTypes.Contains(md.markdownType) || md.localID.IsNil() {
|
|
|
|
|
if debug && md.localID.IsNil() {
|
|
|
|
|
log.Printf("refusing to stash markdown; local ID path is nil: %#v", md)
|
2020-08-21 21:21:48 +00:00
|
|
|
|
}
|
2020-11-25 16:40:24 +00:00
|
|
|
|
break
|
|
|
|
|
}
|
2020-08-21 21:21:48 +00:00
|
|
|
|
|
2020-11-25 16:40:24 +00:00
|
|
|
|
// Checks passed; perform the stash
|
2020-12-11 01:43:31 +00:00
|
|
|
|
m.common.filesStashed[md.localID] = struct{}{}
|
|
|
|
|
m.common.filesStashing[md.localID] = struct{}{}
|
|
|
|
|
cmds = append(cmds, stashDocument(m.common.cc, *md))
|
2020-08-21 21:21:48 +00:00
|
|
|
|
|
2020-11-25 16:40:24 +00:00
|
|
|
|
if m.loadingDone() && !m.spinner.Visible() {
|
|
|
|
|
m.spinner.Start()
|
|
|
|
|
cmds = append(cmds, spinner.Tick)
|
|
|
|
|
}
|
2020-08-21 17:39:59 +00:00
|
|
|
|
|
2020-11-25 16:40:24 +00:00
|
|
|
|
// Prompt for deletion
|
|
|
|
|
case "x":
|
|
|
|
|
m.hideStatusMessage()
|
2020-10-28 00:48:07 +00:00
|
|
|
|
|
2020-11-28 23:30:51 +00:00
|
|
|
|
validState := m.viewState == stashStateReady &&
|
2020-11-26 03:49:49 +00:00
|
|
|
|
m.selectionState == selectionIdle
|
|
|
|
|
|
2020-12-09 20:24:24 +00:00
|
|
|
|
if numDocs == 0 && !validState {
|
2020-11-25 16:40:24 +00:00
|
|
|
|
break
|
|
|
|
|
}
|
2020-05-22 20:28:15 +00:00
|
|
|
|
|
2020-12-10 22:06:01 +00:00
|
|
|
|
md := m.selectedMarkdown()
|
|
|
|
|
if md == nil {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
t := md.markdownType
|
2020-11-28 02:52:51 +00:00
|
|
|
|
if t == StashedDoc || t == ConvertedDoc {
|
2020-11-26 03:49:49 +00:00
|
|
|
|
m.selectionState = selectionPromptingDelete
|
2020-05-22 13:38:34 +00:00
|
|
|
|
}
|
2020-05-15 23:16:22 +00:00
|
|
|
|
|
2020-11-30 17:17:30 +00:00
|
|
|
|
// Toggle full help
|
|
|
|
|
case "?":
|
|
|
|
|
m.showFullHelp = !m.showFullHelp
|
2020-12-08 23:38:10 +00:00
|
|
|
|
m.updatePagination()
|
2020-11-30 17:17:30 +00:00
|
|
|
|
|
2020-11-25 16:40:24 +00:00
|
|
|
|
// Show errors
|
|
|
|
|
case "!":
|
2020-11-28 23:30:51 +00:00
|
|
|
|
if m.err != nil && m.viewState == stashStateReady {
|
|
|
|
|
m.viewState = stashStateShowingError
|
2020-11-25 16:40:24 +00:00
|
|
|
|
return nil
|
2020-07-21 18:55:00 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
2020-11-25 16:40:24 +00:00
|
|
|
|
}
|
2020-07-21 18:55:00 +00:00
|
|
|
|
|
2020-11-25 16:40:24 +00:00
|
|
|
|
// Update paginator. Pagination key handling is done here, but it could
|
|
|
|
|
// also be moved up to this level, in which case we'd use model methods
|
|
|
|
|
// like model.PageUp().
|
2020-12-08 23:38:10 +00:00
|
|
|
|
newPaginatorModel, cmd := m.paginator().Update(msg)
|
|
|
|
|
m.setPaginator(newPaginatorModel)
|
2020-11-25 16:40:24 +00:00
|
|
|
|
cmds = append(cmds, cmd)
|
|
|
|
|
|
|
|
|
|
// Extra paginator keystrokes
|
|
|
|
|
if key, ok := msg.(tea.KeyMsg); ok {
|
|
|
|
|
switch key.String() {
|
|
|
|
|
case "b", "u":
|
2020-12-08 23:38:10 +00:00
|
|
|
|
m.paginator().PrevPage()
|
2020-11-25 16:40:24 +00:00
|
|
|
|
case "f", "d":
|
2020-12-08 23:38:10 +00:00
|
|
|
|
m.paginator().NextPage()
|
2020-05-15 21:31:43 +00:00
|
|
|
|
}
|
2020-11-25 16:40:24 +00:00
|
|
|
|
}
|
2020-05-15 23:16:22 +00:00
|
|
|
|
|
2020-11-25 16:40:24 +00:00
|
|
|
|
// Keep the index in bounds when paginating
|
2020-12-08 23:38:10 +00:00
|
|
|
|
itemsOnPage := m.paginator().ItemsOnPage(len(m.getVisibleMarkdowns()))
|
|
|
|
|
if m.cursor() > itemsOnPage-1 {
|
|
|
|
|
m.setCursor(max(0, itemsOnPage-1))
|
2020-11-25 16:40:24 +00:00
|
|
|
|
}
|
2020-05-15 23:16:22 +00:00
|
|
|
|
|
2020-11-25 16:40:24 +00:00
|
|
|
|
return tea.Batch(cmds...)
|
|
|
|
|
}
|
2020-05-22 13:38:34 +00:00
|
|
|
|
|
2020-11-25 16:40:24 +00:00
|
|
|
|
// Updates for when a user is being prompted whether or not to delete a
|
|
|
|
|
// markdown item.
|
|
|
|
|
func (m *stashModel) handleDeleteConfirmation(msg tea.Msg) tea.Cmd {
|
|
|
|
|
if msg, ok := msg.(tea.KeyMsg); ok {
|
|
|
|
|
switch msg.String() {
|
|
|
|
|
case "y":
|
2020-11-26 03:49:49 +00:00
|
|
|
|
if m.selectionState != selectionPromptingDelete {
|
2020-11-25 16:40:24 +00:00
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
smd := m.selectedMarkdown()
|
|
|
|
|
for i, md := range m.markdowns {
|
|
|
|
|
if md != smd {
|
|
|
|
|
continue
|
2020-08-24 19:59:42 +00:00
|
|
|
|
}
|
2020-05-22 13:38:34 +00:00
|
|
|
|
|
2020-12-10 01:12:01 +00:00
|
|
|
|
// Remove from the things-we-stashed-this-session set
|
2020-12-11 01:43:31 +00:00
|
|
|
|
delete(m.common.filesStashed, md.localID)
|
2020-12-09 20:24:24 +00:00
|
|
|
|
|
|
|
|
|
// Delete optimistically and remove the stashed item before
|
|
|
|
|
// we've received a success response.
|
|
|
|
|
if m.isFiltering() {
|
|
|
|
|
mds, _ := deleteMarkdown(m.filteredMarkdowns, m.markdowns[i])
|
|
|
|
|
m.filteredMarkdowns = mds
|
2020-10-24 20:47:40 +00:00
|
|
|
|
}
|
2020-12-09 20:24:24 +00:00
|
|
|
|
mds, _ := deleteMarkdown(m.markdowns, m.markdowns[i])
|
|
|
|
|
m.markdowns = mds
|
2020-11-25 16:40:24 +00:00
|
|
|
|
}
|
2020-10-24 20:47:40 +00:00
|
|
|
|
|
2020-11-26 03:49:49 +00:00
|
|
|
|
m.selectionState = selectionIdle
|
2020-12-08 23:38:10 +00:00
|
|
|
|
m.updatePagination()
|
2020-05-22 13:38:34 +00:00
|
|
|
|
|
2020-12-11 01:43:31 +00:00
|
|
|
|
return deleteStashedItem(m.common.cc, smd.ID)
|
2020-11-25 16:40:24 +00:00
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
// Any other keys cancels deletion
|
2020-11-26 03:49:49 +00:00
|
|
|
|
m.selectionState = selectionIdle
|
2020-05-22 13:38:34 +00:00
|
|
|
|
}
|
2020-11-25 16:40:24 +00:00
|
|
|
|
}
|
2020-05-22 13:38:34 +00:00
|
|
|
|
|
2020-11-25 16:40:24 +00:00
|
|
|
|
return nil
|
|
|
|
|
}
|
2020-10-24 20:37:18 +00:00
|
|
|
|
|
2020-12-01 03:12:05 +00:00
|
|
|
|
// Updates for when a user is in the filter editing interface.
|
2020-11-25 16:40:24 +00:00
|
|
|
|
func (m *stashModel) handleFiltering(msg tea.Msg) tea.Cmd {
|
|
|
|
|
var cmds []tea.Cmd
|
2020-10-24 20:37:18 +00:00
|
|
|
|
|
2020-11-25 16:40:24 +00:00
|
|
|
|
// Handle keys
|
|
|
|
|
if msg, ok := msg.(tea.KeyMsg); ok {
|
|
|
|
|
switch msg.String() {
|
|
|
|
|
case "esc":
|
|
|
|
|
// Cancel filtering
|
|
|
|
|
m.resetFiltering()
|
|
|
|
|
case "enter", "tab", "shift+tab", "ctrl+k", "up", "ctrl+j", "down":
|
|
|
|
|
m.hideStatusMessage()
|
2020-11-10 23:15:10 +00:00
|
|
|
|
|
2020-11-25 16:40:24 +00:00
|
|
|
|
if len(m.markdowns) == 0 {
|
|
|
|
|
break
|
|
|
|
|
}
|
2020-11-10 23:15:10 +00:00
|
|
|
|
|
2020-11-25 16:40:24 +00:00
|
|
|
|
h := m.getVisibleMarkdowns()
|
2020-10-25 03:51:23 +00:00
|
|
|
|
|
2020-11-25 16:40:24 +00:00
|
|
|
|
// If we've filtered down to nothing, clear the filter
|
|
|
|
|
if len(h) == 0 {
|
2020-11-28 23:30:51 +00:00
|
|
|
|
m.viewState = stashStateReady
|
2020-11-25 16:40:24 +00:00
|
|
|
|
m.resetFiltering()
|
|
|
|
|
break
|
|
|
|
|
}
|
2020-10-24 20:37:18 +00:00
|
|
|
|
|
2020-11-25 16:40:24 +00:00
|
|
|
|
// When there's only one filtered markdown left we can just
|
|
|
|
|
// "open" it directly
|
|
|
|
|
if len(h) == 1 {
|
2020-11-28 23:30:51 +00:00
|
|
|
|
m.viewState = stashStateReady
|
2020-11-25 16:40:24 +00:00
|
|
|
|
m.resetFiltering()
|
|
|
|
|
cmds = append(cmds, m.openMarkdown(h[0]))
|
|
|
|
|
break
|
2020-10-24 20:18:19 +00:00
|
|
|
|
}
|
2020-10-24 20:37:18 +00:00
|
|
|
|
|
2020-11-25 16:40:24 +00:00
|
|
|
|
m.filterInput.Blur()
|
2020-11-23 21:56:44 +00:00
|
|
|
|
|
2020-11-26 01:15:21 +00:00
|
|
|
|
m.filterState = filterApplied
|
2020-11-25 16:40:24 +00:00
|
|
|
|
if m.filterInput.Value() == "" {
|
|
|
|
|
m.resetFiltering()
|
|
|
|
|
}
|
2020-11-23 21:56:44 +00:00
|
|
|
|
}
|
2020-11-25 16:40:24 +00:00
|
|
|
|
}
|
2020-10-24 20:47:40 +00:00
|
|
|
|
|
2020-11-25 16:40:24 +00:00
|
|
|
|
// Update the filter text input component
|
|
|
|
|
newFilterInputModel, inputCmd := m.filterInput.Update(msg)
|
|
|
|
|
currentFilterVal := m.filterInput.Value()
|
|
|
|
|
newFilterVal := newFilterInputModel.Value()
|
|
|
|
|
m.filterInput = newFilterInputModel
|
|
|
|
|
cmds = append(cmds, inputCmd)
|
2020-10-24 20:47:40 +00:00
|
|
|
|
|
2020-11-25 16:40:24 +00:00
|
|
|
|
// If the filtering input has changed, request updated filtering
|
|
|
|
|
if newFilterVal != currentFilterVal {
|
|
|
|
|
cmds = append(cmds, filterMarkdowns(*m))
|
|
|
|
|
}
|
2020-05-22 02:29:46 +00:00
|
|
|
|
|
2020-11-25 16:40:24 +00:00
|
|
|
|
// Update pagination
|
2020-12-08 23:38:10 +00:00
|
|
|
|
m.updatePagination()
|
2020-11-23 21:56:44 +00:00
|
|
|
|
|
2020-11-25 16:40:24 +00:00
|
|
|
|
return tea.Batch(cmds...)
|
|
|
|
|
}
|
2020-05-22 02:29:46 +00:00
|
|
|
|
|
2020-11-25 16:40:24 +00:00
|
|
|
|
func (m *stashModel) handleNoteInput(msg tea.Msg) tea.Cmd {
|
|
|
|
|
var cmds []tea.Cmd
|
|
|
|
|
|
|
|
|
|
if msg, ok := msg.(tea.KeyMsg); ok {
|
|
|
|
|
switch msg.String() {
|
|
|
|
|
case "esc":
|
|
|
|
|
// Cancel note
|
|
|
|
|
m.noteInput.Reset()
|
2020-11-26 03:49:49 +00:00
|
|
|
|
m.selectionState = selectionIdle
|
2020-11-25 16:40:24 +00:00
|
|
|
|
case "enter":
|
|
|
|
|
// Set new note
|
|
|
|
|
md := m.selectedMarkdown()
|
|
|
|
|
newNote := m.noteInput.Value()
|
2020-12-11 01:43:31 +00:00
|
|
|
|
cmd := saveDocumentNote(m.common.cc, md.ID, newNote)
|
2020-11-25 16:40:24 +00:00
|
|
|
|
md.Note = newNote
|
|
|
|
|
m.noteInput.Reset()
|
2020-11-26 03:49:49 +00:00
|
|
|
|
m.selectionState = selectionIdle
|
2020-11-25 16:40:24 +00:00
|
|
|
|
return cmd
|
2020-07-16 19:54:55 +00:00
|
|
|
|
}
|
2020-05-15 00:52:25 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-11-25 16:40:24 +00:00
|
|
|
|
if m.shouldUpdateFilter() {
|
|
|
|
|
cmds = append(cmds, filterMarkdowns(*m))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update the note text input component
|
|
|
|
|
newNoteInputModel, noteInputCmd := m.noteInput.Update(msg)
|
|
|
|
|
m.noteInput = newNoteInputModel
|
|
|
|
|
cmds = append(cmds, noteInputCmd)
|
|
|
|
|
|
|
|
|
|
return tea.Batch(cmds...)
|
2020-05-13 16:53:58 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// VIEW
|
|
|
|
|
|
2020-11-25 16:40:24 +00:00
|
|
|
|
func (m stashModel) view() string {
|
2020-05-13 16:53:58 +00:00
|
|
|
|
var s string
|
2020-11-28 23:30:51 +00:00
|
|
|
|
switch m.viewState {
|
2020-07-16 19:54:55 +00:00
|
|
|
|
case stashStateShowingError:
|
|
|
|
|
return errorView(m.err, false)
|
2020-05-15 19:08:45 +00:00
|
|
|
|
case stashStateLoadingDocument:
|
2020-11-13 21:17:50 +00:00
|
|
|
|
s += " " + m.spinner.View() + " Loading document..."
|
2020-11-28 23:30:51 +00:00
|
|
|
|
case stashStateReady:
|
2020-06-02 22:58:15 +00:00
|
|
|
|
|
2020-10-24 20:37:18 +00:00
|
|
|
|
loadingIndicator := " "
|
2020-12-12 00:54:19 +00:00
|
|
|
|
if !m.loadingDone() || m.spinner.Visible() {
|
2020-11-13 21:17:50 +00:00
|
|
|
|
loadingIndicator = m.spinner.View()
|
2020-07-15 20:41:41 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-08-13 19:37:53 +00:00
|
|
|
|
var header string
|
2020-12-09 20:24:24 +00:00
|
|
|
|
switch m.selectionState {
|
|
|
|
|
case selectionPromptingDelete:
|
|
|
|
|
header = redFg("Delete this item from your stash? ") + faintRedFg("(y/N)")
|
|
|
|
|
case selectionSettingNote:
|
|
|
|
|
header = yellowFg("Set the memo for this item?")
|
2020-05-15 22:34:42 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-07-16 19:09:06 +00:00
|
|
|
|
// Only draw the normal header if we're not using the header area for
|
2020-12-09 20:24:24 +00:00
|
|
|
|
// something else (like a note or delete prompt).
|
2020-07-16 19:09:06 +00:00
|
|
|
|
if header == "" {
|
2020-12-09 20:24:24 +00:00
|
|
|
|
header = m.headerView()
|
2020-07-16 19:09:06 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-12-09 20:24:24 +00:00
|
|
|
|
// Rules for the logo, filter and status message.
|
2020-12-14 23:54:39 +00:00
|
|
|
|
logoOrFilter := " "
|
|
|
|
|
if m.showStatusMessage && m.isFiltering() {
|
|
|
|
|
logoOrFilter += m.statusMessage.String()
|
2020-12-09 20:24:24 +00:00
|
|
|
|
} else if m.isFiltering() {
|
2020-12-14 23:54:39 +00:00
|
|
|
|
logoOrFilter += m.filterInput.View()
|
2020-12-09 20:24:24 +00:00
|
|
|
|
} else {
|
2020-12-14 23:54:39 +00:00
|
|
|
|
logoOrFilter += glowLogoView(" Glow ")
|
|
|
|
|
if m.showStatusMessage {
|
|
|
|
|
logoOrFilter += " " + m.statusMessage.String()
|
|
|
|
|
}
|
2020-10-24 20:37:18 +00:00
|
|
|
|
}
|
2020-12-14 23:54:39 +00:00
|
|
|
|
logoOrFilter = truncate.StringWithTail(logoOrFilter, uint(m.common.width), ellipsis)
|
2020-10-24 20:37:18 +00:00
|
|
|
|
|
2020-12-03 23:15:59 +00:00
|
|
|
|
help, helpHeight := m.helpView()
|
|
|
|
|
|
|
|
|
|
populatedView := m.populatedView()
|
|
|
|
|
populatedViewHeight := strings.Count(populatedView, "\n") + 2
|
|
|
|
|
|
|
|
|
|
// We need to fill any empty height with newlines so the footer reaches
|
|
|
|
|
// the bottom.
|
2020-12-11 01:43:31 +00:00
|
|
|
|
availHeight := m.common.height -
|
2020-12-03 23:15:59 +00:00
|
|
|
|
stashViewTopPadding -
|
|
|
|
|
populatedViewHeight -
|
|
|
|
|
helpHeight -
|
|
|
|
|
stashViewBottomPadding
|
|
|
|
|
blankLines := strings.Repeat("\n", max(0, availHeight))
|
|
|
|
|
|
2020-05-15 23:16:22 +00:00
|
|
|
|
var pagination string
|
2020-12-08 23:38:10 +00:00
|
|
|
|
if m.paginator().TotalPages > 1 {
|
|
|
|
|
pagination = m.paginator().View()
|
2020-05-15 23:16:22 +00:00
|
|
|
|
|
2020-07-20 15:00:17 +00:00
|
|
|
|
// If the dot pagination is wider than the width of the window
|
|
|
|
|
// switch to the arabic paginator.
|
2020-12-11 01:43:31 +00:00
|
|
|
|
if ansi.PrintableRuneWidth(pagination) > m.common.width-stashViewHorizontalPadding {
|
2020-12-08 23:38:10 +00:00
|
|
|
|
m.paginator().Type = paginator.Arabic
|
2020-12-11 01:43:31 +00:00
|
|
|
|
pagination = lib.Subtle(m.paginator().View())
|
2020-07-20 15:00:17 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-08-13 16:32:48 +00:00
|
|
|
|
// We could also look at m.stashFullyLoaded and add an indicator
|
|
|
|
|
// showing that we don't actually know how many more pages there
|
|
|
|
|
// are.
|
2020-05-15 22:44:22 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-05-15 00:52:25 +00:00
|
|
|
|
s += fmt.Sprintf(
|
2020-12-14 23:54:39 +00:00
|
|
|
|
"%s%s\n\n %s\n\n%s\n\n%s %s\n\n%s",
|
2020-07-15 20:41:41 +00:00
|
|
|
|
loadingIndicator,
|
2020-11-13 21:33:22 +00:00
|
|
|
|
logoOrFilter,
|
2020-05-15 22:34:42 +00:00
|
|
|
|
header,
|
2020-12-03 23:15:59 +00:00
|
|
|
|
populatedView,
|
2020-05-15 01:12:13 +00:00
|
|
|
|
blankLines,
|
2020-05-15 23:16:22 +00:00
|
|
|
|
pagination,
|
2020-11-30 17:17:30 +00:00
|
|
|
|
help,
|
2020-05-15 00:52:25 +00:00
|
|
|
|
)
|
2020-05-13 23:02:39 +00:00
|
|
|
|
}
|
2020-07-16 19:09:06 +00:00
|
|
|
|
return "\n" + indent(s, stashIndent)
|
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).
|
2020-12-11 01:43:31 +00:00
|
|
|
|
Background(lib.Fuschia.Color()).
|
2020-05-19 17:20:39 +00:00
|
|
|
|
String()
|
|
|
|
|
}
|
|
|
|
|
|
2020-12-09 20:24:24 +00:00
|
|
|
|
func (m stashModel) headerView() string {
|
|
|
|
|
localCount := m.countMarkdowns(LocalDoc)
|
|
|
|
|
stashedCount := m.countMarkdowns(StashedDoc) + m.countMarkdowns(ConvertedDoc)
|
|
|
|
|
newsCount := m.countMarkdowns(NewsDoc)
|
|
|
|
|
|
|
|
|
|
var sections []string
|
|
|
|
|
|
|
|
|
|
// Filter results
|
|
|
|
|
if m.isFiltering() {
|
|
|
|
|
if localCount+stashedCount+newsCount == 0 {
|
|
|
|
|
return grayFg("Nothing found.")
|
|
|
|
|
} else {
|
|
|
|
|
if localCount > 0 {
|
|
|
|
|
sections = append(sections, fmt.Sprintf("%d local", localCount))
|
|
|
|
|
}
|
|
|
|
|
if stashedCount > 0 {
|
|
|
|
|
sections = append(sections, fmt.Sprintf("%d stashed", stashedCount))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for i := range sections {
|
|
|
|
|
sections[i] = grayFg(sections[i])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return strings.Join(sections, dividerDot)
|
|
|
|
|
}
|
2020-07-16 19:09:06 +00:00
|
|
|
|
|
2020-12-03 23:15:59 +00:00
|
|
|
|
if m.loadingDone() && len(m.markdowns) == 0 {
|
|
|
|
|
var maybeOffline string
|
2020-12-11 01:43:31 +00:00
|
|
|
|
if m.common.authStatus == authFailed {
|
2020-12-03 23:15:59 +00:00
|
|
|
|
maybeOffline = " " + offlineHeaderNote
|
|
|
|
|
}
|
2020-09-10 17:32:57 +00:00
|
|
|
|
|
2020-10-18 01:46:02 +00:00
|
|
|
|
if m.stashedOnly() {
|
2020-12-11 01:43:31 +00:00
|
|
|
|
return lib.Subtle("Can’t load stash") + maybeOffline
|
2020-09-07 23:32:19 +00:00
|
|
|
|
}
|
2020-12-11 01:43:31 +00:00
|
|
|
|
return lib.Subtle("No markdown files found") + maybeOffline
|
2020-07-16 19:09:06 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-12-09 20:24:24 +00:00
|
|
|
|
// Tabs
|
2020-12-03 23:15:59 +00:00
|
|
|
|
for i, v := range m.sections {
|
|
|
|
|
var s string
|
|
|
|
|
|
2020-12-08 23:38:10 +00:00
|
|
|
|
switch v.key {
|
|
|
|
|
case localSection:
|
2020-12-03 23:15:59 +00:00
|
|
|
|
if m.stashedOnly() {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
s = fmt.Sprintf("%d local", localCount)
|
2020-12-08 23:38:10 +00:00
|
|
|
|
case stashedSection:
|
2020-12-03 23:15:59 +00:00
|
|
|
|
if m.localOnly() {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
s = fmt.Sprintf("%d stashed", stashedCount)
|
2020-12-08 23:38:10 +00:00
|
|
|
|
case newsSection:
|
2020-12-03 23:15:59 +00:00
|
|
|
|
if m.localOnly() {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
s = fmt.Sprintf("%d news", newsCount)
|
2020-09-07 23:14:59 +00:00
|
|
|
|
}
|
2020-07-16 19:09:06 +00:00
|
|
|
|
|
2020-12-08 23:38:10 +00:00
|
|
|
|
if m.sectionIndex == i && len(m.sections) > 1 {
|
2020-12-03 23:15:59 +00:00
|
|
|
|
s = brightGrayFg(s)
|
|
|
|
|
} else {
|
|
|
|
|
s = grayFg(s)
|
2020-07-16 19:09:06 +00:00
|
|
|
|
}
|
2020-12-03 23:15:59 +00:00
|
|
|
|
sections = append(sections, s)
|
2020-07-16 19:09:06 +00:00
|
|
|
|
}
|
2020-10-25 03:57:12 +00:00
|
|
|
|
|
2020-12-03 23:15:59 +00:00
|
|
|
|
s := strings.Join(sections, dividerBar)
|
2020-12-11 01:43:31 +00:00
|
|
|
|
if m.common.authStatus == authFailed {
|
2020-12-03 23:15:59 +00:00
|
|
|
|
s += dividerDot + offlineHeaderNote
|
2020-10-25 03:57:12 +00:00
|
|
|
|
}
|
2020-12-03 23:15:59 +00:00
|
|
|
|
|
|
|
|
|
return s
|
2020-05-13 23:02:39 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-11-28 23:30:51 +00:00
|
|
|
|
func (m stashModel) populatedView() string {
|
2020-11-23 21:56:44 +00:00
|
|
|
|
mds := m.getVisibleMarkdowns()
|
2020-12-03 23:15:59 +00:00
|
|
|
|
|
2020-12-09 20:24:24 +00:00
|
|
|
|
if len(mds) == 0 && m.isFiltering() {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var b strings.Builder
|
|
|
|
|
|
2020-12-03 23:15:59 +00:00
|
|
|
|
// Empty states
|
|
|
|
|
if len(mds) == 0 {
|
|
|
|
|
f := func(s string) {
|
|
|
|
|
b.WriteString(" " + grayFg(s))
|
|
|
|
|
}
|
|
|
|
|
|
2020-12-08 23:38:10 +00:00
|
|
|
|
switch m.sections[m.sectionIndex].key {
|
|
|
|
|
case localSection:
|
2020-12-03 23:15:59 +00:00
|
|
|
|
if m.loadingDone() {
|
|
|
|
|
f("No local files found.")
|
|
|
|
|
} else {
|
|
|
|
|
f("Looking for local files...")
|
|
|
|
|
}
|
2020-12-08 23:38:10 +00:00
|
|
|
|
case stashedSection:
|
2020-12-11 01:43:31 +00:00
|
|
|
|
if m.common.authStatus == authFailed {
|
2020-12-03 23:15:59 +00:00
|
|
|
|
f("Can't load your stash. Are you offline?")
|
|
|
|
|
} else if m.loadingDone() {
|
|
|
|
|
f("Nothing stashed yet.")
|
|
|
|
|
} else {
|
|
|
|
|
f("Loading your stash...")
|
|
|
|
|
}
|
2020-12-08 23:38:10 +00:00
|
|
|
|
case newsSection:
|
2020-12-11 01:43:31 +00:00
|
|
|
|
if m.common.authStatus == authFailed {
|
2020-12-03 23:15:59 +00:00
|
|
|
|
f("Can't load news. Are you offline?")
|
|
|
|
|
} else if m.loadingDone() {
|
|
|
|
|
f("No stashed files found.")
|
|
|
|
|
} else {
|
|
|
|
|
f("Loading your stash...")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-23 21:56:44 +00:00
|
|
|
|
if len(mds) > 0 {
|
2020-12-08 23:38:10 +00:00
|
|
|
|
start, end := m.paginator().GetSliceBounds(len(mds))
|
2020-11-23 21:56:44 +00:00
|
|
|
|
docs := mds[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-12-08 23:38:10 +00:00
|
|
|
|
itemsOnPage := m.paginator().ItemsOnPage(len(mds))
|
|
|
|
|
if itemsOnPage < m.paginator().PerPage {
|
|
|
|
|
n := (m.paginator().PerPage - itemsOnPage) * stashViewItemHeight
|
2020-11-23 21:56:44 +00:00
|
|
|
|
if len(mds) == 0 {
|
2020-06-02 23:44:26 +00:00
|
|
|
|
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-08-24 20:48:10 +00:00
|
|
|
|
// COMMANDS
|
2020-05-13 16:53:58 +00:00
|
|
|
|
|
2020-12-10 01:12:01 +00:00
|
|
|
|
// loadRemoteMarkdown is a command for loading markdown from the server.
|
|
|
|
|
func loadRemoteMarkdown(cc *charm.Client, md *markdown) tea.Cmd {
|
2020-05-26 17:42:25 +00:00
|
|
|
|
return func() tea.Msg {
|
2020-12-11 03:53:24 +00:00
|
|
|
|
newMD, err := fetchMarkdown(cc, md.ID, md.markdownType)
|
2020-05-21 19:14:33 +00:00
|
|
|
|
if err != nil {
|
2020-08-18 19:41:57 +00:00
|
|
|
|
if debug {
|
2020-12-10 01:12:01 +00:00
|
|
|
|
log.Printf("error loading %s markdown (ID %d, Note: '%s'): %v", md.markdownType, md.ID, md.Note, err)
|
|
|
|
|
}
|
|
|
|
|
return markdownFetchFailedMsg{
|
|
|
|
|
err: err,
|
|
|
|
|
id: md.ID,
|
|
|
|
|
note: md.Note,
|
2020-08-18 19:41:57 +00:00
|
|
|
|
}
|
2020-05-21 19:14:33 +00:00
|
|
|
|
}
|
2020-12-10 16:26:26 +00:00
|
|
|
|
newMD.localID = md.localID
|
|
|
|
|
return fetchedMarkdownMsg(newMD)
|
2020-12-10 01:12:01 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-07-14 20:05:21 +00:00
|
|
|
|
func loadLocalMarkdown(md *markdown) tea.Cmd {
|
|
|
|
|
return func() tea.Msg {
|
2020-11-28 02:52:51 +00:00
|
|
|
|
if md.markdownType != LocalDoc {
|
2020-08-21 20:13:38 +00:00
|
|
|
|
return errMsg{errors.New("could not load local file: not a local file")}
|
2020-07-14 20:05:21 +00:00
|
|
|
|
}
|
|
|
|
|
if md.localPath == "" {
|
2020-08-21 20:13:38 +00:00
|
|
|
|
return errMsg{errors.New("could not load file: missing path")}
|
2020-07-14 20:05:21 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
data, err := ioutil.ReadFile(md.localPath)
|
|
|
|
|
if err != nil {
|
2020-08-18 19:41:57 +00:00
|
|
|
|
if debug {
|
|
|
|
|
log.Println("error reading local markdown:", err)
|
|
|
|
|
}
|
2020-08-21 20:13:38 +00:00
|
|
|
|
return errMsg{err}
|
2020-07-14 20:05:21 +00:00
|
|
|
|
}
|
|
|
|
|
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-09-09 23:43:03 +00:00
|
|
|
|
if debug {
|
2020-08-24 19:59:28 +00:00
|
|
|
|
log.Println("could not delete stashed item:", err)
|
|
|
|
|
}
|
2020-08-21 20:13:38 +00:00
|
|
|
|
return errMsg{err}
|
2020-05-15 22:34:42 +00:00
|
|
|
|
}
|
|
|
|
|
return deletedStashedItemMsg(id)
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-05-18 20:08:49 +00:00
|
|
|
|
|
2020-11-23 21:56:44 +00:00
|
|
|
|
func filterMarkdowns(m stashModel) tea.Cmd {
|
|
|
|
|
return func() tea.Msg {
|
|
|
|
|
if m.filterInput.Value() == "" || !m.isFiltering() {
|
2020-11-25 16:40:24 +00:00
|
|
|
|
return filteredMarkdownMsg(m.getFilterableMarkdowns()) // return everything
|
2020-11-23 21:56:44 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
targets := []string{}
|
2020-11-25 16:40:24 +00:00
|
|
|
|
mds := m.getFilterableMarkdowns()
|
2020-11-23 21:56:44 +00:00
|
|
|
|
|
2020-11-25 16:40:24 +00:00
|
|
|
|
for _, t := range mds {
|
2020-11-23 21:56:44 +00:00
|
|
|
|
targets = append(targets, t.filterValue)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ranks := fuzzy.Find(m.filterInput.Value(), targets)
|
|
|
|
|
sort.Stable(ranks)
|
|
|
|
|
|
|
|
|
|
filtered := []*markdown{}
|
|
|
|
|
for _, r := range ranks {
|
2020-11-25 16:40:24 +00:00
|
|
|
|
filtered = append(filtered, mds[r.Index])
|
2020-11-23 21:56:44 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return filteredMarkdownMsg(filtered)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-18 20:08:49 +00:00
|
|
|
|
// ETC
|
|
|
|
|
|
2020-12-11 03:53:24 +00:00
|
|
|
|
// fetchMarkdown performs the actual I/O for loading markdown from the sever.
|
|
|
|
|
func fetchMarkdown(cc *charm.Client, id int, t DocType) (*markdown, error) {
|
|
|
|
|
var md *charm.Markdown
|
|
|
|
|
var err error
|
|
|
|
|
|
|
|
|
|
switch t {
|
|
|
|
|
case StashedDoc, ConvertedDoc:
|
|
|
|
|
md, err = cc.GetStashMarkdown(id)
|
|
|
|
|
case NewsDoc:
|
|
|
|
|
md, err = cc.GetNewsMarkdown(id)
|
|
|
|
|
default:
|
|
|
|
|
err = fmt.Errorf("unknown markdown type: %s", t)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &markdown{
|
|
|
|
|
markdownType: t,
|
|
|
|
|
Markdown: *md,
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
2020-12-01 03:12:05 +00:00
|
|
|
|
// Delete a markdown from a slice of markdowns.
|
2020-11-23 21:56:44 +00:00
|
|
|
|
func deleteMarkdown(markdowns []*markdown, target *markdown) ([]*markdown, error) {
|
|
|
|
|
index := -1
|
|
|
|
|
|
|
|
|
|
for i, v := range markdowns {
|
|
|
|
|
switch target.markdownType {
|
2020-12-09 20:24:24 +00:00
|
|
|
|
case ConvertedDoc:
|
|
|
|
|
if v.markdownType == ConvertedDoc && v.localPath == target.localPath {
|
2020-11-23 21:56:44 +00:00
|
|
|
|
index = i
|
|
|
|
|
}
|
2020-12-09 20:24:24 +00:00
|
|
|
|
case StashedDoc:
|
2020-11-23 21:56:44 +00:00
|
|
|
|
if v.ID == target.ID {
|
|
|
|
|
index = i
|
|
|
|
|
}
|
|
|
|
|
default:
|
2020-12-10 01:12:01 +00:00
|
|
|
|
return nil, fmt.Errorf("%s documents cannot be deleted", target.markdownType)
|
2020-11-23 21:56:44 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if index == -1 {
|
|
|
|
|
err := fmt.Errorf("could not find markdown to delete")
|
|
|
|
|
if debug {
|
|
|
|
|
log.Println(err)
|
|
|
|
|
}
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return append(markdowns[:index], markdowns[index+1:]...), nil
|
|
|
|
|
}
|