diff --git a/ui/markdown.go b/ui/markdown.go index 3d2627c..5d2a232 100644 --- a/ui/markdown.go +++ b/ui/markdown.go @@ -19,10 +19,16 @@ import ( type markdown struct { markdownType DocType - // Local identifier. This allows us to precisely determine the stashed - // state of a markdown, regardless of whether it exists locally or on the - // network. - localID ksuid.KSUID + // Stash identifier. This exists so we can keep track of documents stashed + // in-session as they relate to their original, non-stashed counterparts. + // All documents have a stashID, however when a document is stashed that + // document inherits the stashID of the original. + stashID ksuid.KSUID + + // Unique identifier. Unlike the stash identifier, this value should always + // be unique so we can confidently find it an operate on it (versus stashID, + // which could match both an original or stashed document). + uniqueID ksuid.KSUID // Full path of a local markdown file. Only relevant to local documents and // those that have been stashed in this session. @@ -36,10 +42,11 @@ type markdown struct { charm.Markdown } -func (m *markdown) generateLocalID() { - if m.localID.IsNil() { - m.localID = ksuid.New() +func (m *markdown) generateIDs() { + if m.stashID.IsNil() { + m.stashID = ksuid.New() } + m.uniqueID = ksuid.New() } // Generate the value we're doing to filter against. @@ -55,9 +62,9 @@ func (m *markdown) buildFilterValue() { m.filterValue = note } -// sortAsLocal returns whether or not this markdown should be sorted as though +// shouldSortAsLocal returns whether or not this markdown should be sorted as though // it's a local markdown document. -func (m markdown) sortAsLocal() bool { +func (m markdown) shouldSortAsLocal() bool { return m.markdownType == LocalDoc || m.markdownType == ConvertedDoc } @@ -67,8 +74,8 @@ 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 { - iIsLocal := m[i].sortAsLocal() - jIsLocal := m[j].sortAsLocal() + iIsLocal := m[i].shouldSortAsLocal() + jIsLocal := m[j].shouldSortAsLocal() // Local files (and files that used to be local) come first if iIsLocal && !jIsLocal { @@ -89,10 +96,10 @@ func (m markdownsByLocalFirst) Less(i, j int) bool { return m[i].CreatedAt.After(m[j].CreatedAt) } - // If the timestamps also match, sort by local ID. - localIDs := []ksuid.KSUID{m[i].localID, m[j].localID} - ksuid.Sort(localIDs) - return localIDs[0] == m[i].localID + // If the times also match, sort by unqiue ID. + ids := []ksuid.KSUID{m[i].uniqueID, m[j].uniqueID} + ksuid.Sort(ids) + return ids[0] == m[i].uniqueID } func (m markdown) relativeTime() string { diff --git a/ui/pager.go b/ui/pager.go index 3192264..8494c23 100644 --- a/ui/pager.go +++ b/ui/pager.go @@ -242,7 +242,7 @@ func (m pagerModel) update(msg tea.Msg) (pagerModel, tea.Cmd) { md := m.currentDocument - _, alreadyStashed := m.common.filesStashed[md.localID] + _, alreadyStashed := m.common.filesStashed[md.stashID] if alreadyStashed { cmds = append(cmds, m.showStatusMessage("Already stashed")) break @@ -314,7 +314,7 @@ func (m pagerModel) update(msg tea.Msg) (pagerModel, tea.Cmd) { } case stashFailMsg: - delete(m.common.filesStashed, msg.markdown.localID) + delete(m.common.filesStashed, msg.markdown.stashID) case statusMessageTimeoutMsg: m.state = pagerStateBrowse diff --git a/ui/stash.go b/ui/stash.go index e30082b..5d100ff 100644 --- a/ui/stash.go +++ b/ui/stash.go @@ -71,6 +71,7 @@ const ( localSection = iota stashedSection newsSection + filterSection ) // section contains definitions and state information for displaying a tab and @@ -85,16 +86,24 @@ type section struct { // map sections to their associated types. var sections = map[sectionKey]section{ localSection: { - key: localSection, - docTypes: NewDocTypeSet(LocalDoc), + key: localSection, + docTypes: NewDocTypeSet(LocalDoc), + paginator: newStashPaginator(), }, stashedSection: { - key: stashedSection, - docTypes: NewDocTypeSet(StashedDoc, ConvertedDoc), + key: stashedSection, + docTypes: NewDocTypeSet(StashedDoc, ConvertedDoc), + paginator: newStashPaginator(), }, newsSection: { - key: newsSection, - docTypes: NewDocTypeSet(NewsDoc), + key: newsSection, + docTypes: NewDocTypeSet(NewsDoc), + paginator: newStashPaginator(), + }, + filterSection: { + key: filterSection, + docTypes: DocTypeSet{}, + paginator: newStashPaginator(), }, } @@ -246,10 +255,17 @@ func (m *stashModel) resetFiltering() { sort.Stable(markdownsByLocalFirst(m.markdowns)) m.filteredMarkdowns = nil m.updatePagination() + + if m.sections[len(m.sections)-1].key == filterSection { + m.sections = m.sections[:len(m.sections)-1] + } + if m.sectionIndex > len(m.sections)-1 { + m.sectionIndex = 0 + } } // Is a filter currently being applied? -func (m stashModel) isFiltering() bool { +func (m stashModel) filterApplied() bool { return m.filterState != unfiltered } @@ -257,7 +273,7 @@ func (m stashModel) isFiltering() bool { 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. - return m.isFiltering() && m.selectionState != selectionSettingNote + return m.filterApplied() && m.selectionState != selectionSettingNote } // Update pagination according to the amount of markdowns for the current @@ -305,11 +321,11 @@ func (m stashModel) selectedMarkdown() *markdown { func (m *stashModel) addMarkdowns(mds ...*markdown) { if len(mds) > 0 { for _, md := range mds { - md.generateLocalID() + md.generateIDs() } m.markdowns = append(m.markdowns, mds...) - if !m.isFiltering() { + if !m.filterApplied() { sort.Stable(markdownsByLocalFirst(m.markdowns)) } m.updatePagination() @@ -323,7 +339,7 @@ func (m stashModel) countMarkdowns(t DocType) (found int) { } var mds []*markdown - if m.isFiltering() { + if m.filterState == filtering { mds = m.getVisibleMarkdowns() } else { mds = m.markdowns @@ -359,7 +375,7 @@ func (m stashModel) getMarkdownByType(types ...DocType) []*markdown { // Returns the markdowns that should be currently shown. func (m stashModel) getVisibleMarkdowns() []*markdown { - if m.isFiltering() { + if m.filterState == filtering || m.currentSection().key == filterSection { return m.filteredMarkdowns } @@ -485,15 +501,6 @@ func newStashModel(common *commonModel) stashModel { } } - p := paginator.NewModel() - p.Type = paginator.Dots - p.ActiveDot = brightGrayFg("•") - p.InactiveDot = darkGrayFg("•") - - for i := range s { - s[i].paginator = p - } - m := stashModel{ common: common, spinner: sp, @@ -507,6 +514,14 @@ func newStashModel(common *commonModel) stashModel { return m } +func newStashPaginator() paginator.Model { + p := paginator.NewModel() + p.Type = paginator.Dots + p.ActiveDot = brightGrayFg("•") + p.InactiveDot = darkGrayFg("•") + return p +} + // UPDATE func (m stashModel) update(msg tea.Msg) (stashModel, tea.Cmd) { @@ -558,7 +573,7 @@ func (m stashModel) update(msg tea.Msg) (stashModel, tea.Cmd) { // If we're filtering build filter indexes immediately so any // matching results will show up in the filter. - if m.isFiltering() { + if m.filterApplied() { for _, md := range docs { md.buildFilterValue() } @@ -692,14 +707,15 @@ func (m *stashModel) handleDocumentBrowsing(msg tea.Msg) tea.Cmd { m.paginator().Page = m.paginator().TotalPages - 1 m.setCursor(m.paginator().ItemsOnPage(numDocs) - 1) + // Clear filter (if applicable) case "esc": - if m.isFiltering() { + if m.filterApplied() { m.resetFiltering() - break } + // Next section case "tab": - if len(m.sections) == 0 || m.isFiltering() { + if len(m.sections) == 0 || m.filterState == filtering { break } m.sectionIndex++ @@ -708,8 +724,9 @@ func (m *stashModel) handleDocumentBrowsing(msg tea.Msg) tea.Cmd { } m.updatePagination() + // Previous section case "shift+tab": - if len(m.sections) == 0 { + if len(m.sections) == 0 || m.filterState == filtering { break } m.sectionIndex-- @@ -781,21 +798,21 @@ func (m *stashModel) handleDocumentBrowsing(msg tea.Msg) tea.Cmd { break } - if _, alreadyStashed := m.common.filesStashed[md.localID]; alreadyStashed { + if _, alreadyStashed := m.common.filesStashed[md.stashID]; alreadyStashed { cmds = append(cmds, m.newStatusMessage(alreadyStashedStatusMessage)) break } - if !stashableDocTypes.Contains(md.markdownType) || md.localID.IsNil() { - if debug && md.localID.IsNil() { + if !stashableDocTypes.Contains(md.markdownType) || md.stashID.IsNil() { + if debug && md.stashID.IsNil() { log.Printf("refusing to stash markdown; local ID path is nil: %#v", md) } break } // Checks passed; perform the stash - m.common.filesStashed[md.localID] = struct{}{} - m.common.filesStashing[md.localID] = struct{}{} + m.common.filesStashed[md.stashID] = struct{}{} + m.common.filesStashing[md.stashID] = struct{}{} cmds = append(cmds, stashDocument(m.common.cc, *md)) if m.loadingDone() && !m.spinner.Visible() { @@ -881,11 +898,11 @@ func (m *stashModel) handleDeleteConfirmation(msg tea.Msg) tea.Cmd { } // Remove from the things-we-stashed-this-session set - delete(m.common.filesStashed, md.localID) + delete(m.common.filesStashed, md.stashID) // Delete optimistically and remove the stashed item before // we've received a success response. - if m.isFiltering() { + if m.filterApplied() { mds, _ := deleteMarkdown(m.filteredMarkdowns, m.markdowns[i]) m.filteredMarkdowns = mds } @@ -896,6 +913,10 @@ func (m *stashModel) handleDeleteConfirmation(msg tea.Msg) tea.Cmd { m.selectionState = selectionIdle m.updatePagination() + if len(m.filteredMarkdowns) == 0 { + m.resetFiltering() + } + return deleteStashedItem(m.common.cc, smd.ID) // Any other key cancels deletion @@ -942,6 +963,12 @@ func (m *stashModel) handleFiltering(msg tea.Msg) tea.Cmd { break } + // Add new section if it's not present + if m.sections[len(m.sections)-1].key != filterSection { + m.sections = append(m.sections, sections[filterSection]) + } + m.sectionIndex = len(m.sections) - 1 + m.filterInput.Blur() m.filterState = filterApplied @@ -1034,9 +1061,9 @@ func (m stashModel) view() string { // Rules for the logo, filter and status message. logoOrFilter := " " - if m.showStatusMessage && m.isFiltering() { + if m.showStatusMessage && m.filterState == filtering { logoOrFilter += m.statusMessage.String() - } else if m.isFiltering() { + } else if m.filterState == filtering { logoOrFilter += m.filterInput.View() } else { logoOrFilter += glowLogoView(" Glow ") @@ -1106,7 +1133,7 @@ func (m stashModel) headerView() string { var sections []string // Filter results - if m.isFiltering() { + if m.filterState == filtering { if localCount+stashedCount+newsCount == 0 { return grayFg("Nothing found.") } else { @@ -1157,6 +1184,8 @@ func (m stashModel) headerView() string { continue } s = fmt.Sprintf("%d news", newsCount) + case filterSection: + s = fmt.Sprintf("%d “%s”", len(m.filteredMarkdowns), m.filterInput.Value()) } if m.sectionIndex == i && len(m.sections) > 1 { @@ -1178,7 +1207,7 @@ func (m stashModel) headerView() string { func (m stashModel) populatedView() string { mds := m.getVisibleMarkdowns() - if len(mds) == 0 && m.isFiltering() { + if len(mds) == 0 && m.filterApplied() { return "" } @@ -1261,7 +1290,7 @@ func loadRemoteMarkdown(cc *charm.Client, md *markdown) tea.Cmd { note: md.Note, } } - newMD.localID = md.localID + newMD.stashID = md.stashID return fetchedMarkdownMsg(newMD) } } @@ -1302,7 +1331,7 @@ func deleteStashedItem(cc *charm.Client, id int) tea.Cmd { func filterMarkdowns(m stashModel) tea.Cmd { return func() tea.Msg { - if m.filterInput.Value() == "" || !m.isFiltering() { + if m.filterInput.Value() == "" || !m.filterApplied() { return filteredMarkdownMsg(m.getFilterableMarkdowns()) // return everything } @@ -1356,7 +1385,7 @@ func deleteMarkdown(markdowns []*markdown, target *markdown) ([]*markdown, error index := -1 for i, v := range markdowns { - if v.localID == target.localID { + if v.uniqueID == target.uniqueID { index = i break } diff --git a/ui/ui.go b/ui/ui.go index bd13537..9a0dc96 100644 --- a/ui/ui.go +++ b/ui/ui.go @@ -249,7 +249,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case stateShowStash: // Q quits if we're filtering, but we still send esc though. - if m.stash.isFiltering() { + if m.stash.filterApplied() { if msg.String() == "q" { return m, tea.Quit } @@ -395,7 +395,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case foundLocalFileMsg: newMd := localFileToMarkdown(m.common.cwd, gitcha.SearchResult(msg)) m.stash.addMarkdowns(newMd) - if m.stash.isFiltering() { + if m.stash.filterApplied() { newMd.buildFilterValue() } if m.stash.shouldUpdateFilter() { @@ -407,17 +407,17 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // Common handling that should happen regardless of application state md := markdown(msg) m.stash.addMarkdowns(&md) - m.common.filesStashed[msg.localID] = struct{}{} - delete(m.common.filesStashing, md.localID) + m.common.filesStashed[msg.stashID] = struct{}{} + delete(m.common.filesStashing, md.stashID) - if m.stash.isFiltering() { + if m.stash.filterApplied() { cmds = append(cmds, filterMarkdowns(m.stash)) } case stashFailMsg: // Common handling that should happen regardless of application state - delete(m.common.filesStashed, msg.markdown.localID) - delete(m.common.filesStashing, msg.markdown.localID) + delete(m.common.filesStashed, msg.markdown.stashID) + delete(m.common.filesStashing, msg.markdown.stashID) case filteredMarkdownMsg: if m.state == stateShowDocument {