From 88806c8abcbad176499ef54fd6c09e7a2df3ff11 Mon Sep 17 00:00:00 2001 From: Christian Rocha Date: Wed, 9 Dec 2020 20:12:01 -0500 Subject: [PATCH] Make news stashable --- go.mod | 1 + go.sum | 4 ++ ui/markdown.go | 12 ++++++ ui/stash.go | 104 +++++++++++++++++++++++++++++++------------------ ui/ui.go | 55 +++++++++++++++++++------- 5 files changed, 124 insertions(+), 52 deletions(-) diff --git a/go.mod b/go.mod index fbf0a23a..6172ba01 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,7 @@ require ( github.com/muesli/reflow v0.2.0 github.com/muesli/termenv v0.7.4 github.com/sahilm/fuzzy v0.1.0 + github.com/segmentio/ksuid v1.0.3 github.com/spf13/cobra v1.1.1 github.com/spf13/viper v1.7.0 golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 diff --git a/go.sum b/go.sum index ed099fb8..024a2d0c 100644 --- a/go.sum +++ b/go.sum @@ -241,7 +241,11 @@ github.com/sabhiram/go-gitignore v0.0.0-20180611051255-d3107576ba94 h1:G04eS0JkA github.com/sabhiram/go-gitignore v0.0.0-20180611051255-d3107576ba94/go.mod h1:b18R55ulyQ/h3RaWyloPyER7fWQVZvimKKhnI5OfrJQ= github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI= github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= +github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/segmentio/ksuid v1.0.3 h1:FoResxvleQwYiPAVKe1tMUlEirodZqlqglIuFsdDntY= +github.com/segmentio/ksuid v1.0.3/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= diff --git a/ui/markdown.go b/ui/markdown.go index f9f0909e..9b82de9c 100644 --- a/ui/markdown.go +++ b/ui/markdown.go @@ -9,6 +9,7 @@ import ( "github.com/charmbracelet/charm" "github.com/dustin/go-humanize" + "github.com/segmentio/ksuid" "golang.org/x/text/runes" "golang.org/x/text/transform" "golang.org/x/text/unicode/norm" @@ -18,6 +19,11 @@ 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 + // Full path of a local markdown file. Only relevant to local documents and // those that have been stashed in this session. localPath string @@ -30,6 +36,12 @@ type markdown struct { charm.Markdown } +func (m *markdown) generateLocalID() { + if m.localID.IsNil() { + m.localID = ksuid.New() + } +} + // Generate the value we're doing to filter against. func (m *markdown) buildFilterValue() { note, err := normalize(m.Note) diff --git a/ui/stash.go b/ui/stash.go index 65b3f6b2..b001d8ea 100644 --- a/ui/stash.go +++ b/ui/stash.go @@ -18,6 +18,7 @@ import ( "github.com/muesli/reflow/ansi" te "github.com/muesli/termenv" "github.com/sahilm/fuzzy" + "github.com/segmentio/ksuid" ) const ( @@ -37,9 +38,15 @@ var ( // MSG -type fetchedMarkdownMsg *markdown type deletedStashedItemMsg int type filteredMarkdownMsg []*markdown +type fetchedMarkdownMsg *markdown + +type markdownFetchFailedMsg struct { + err error + id int + note string +} // MODEL @@ -138,7 +145,7 @@ type stashModel struct { // Paths to files stashed this session. We treat this like a set, ignoring // the value portion with an empty struct. - filesStashed map[string]struct{} + filesStashed map[ksuid.KSUID]struct{} // Page we're fetching stash items from on the server, which is different // from the local pagination. Generally, the server will return more items @@ -268,6 +275,10 @@ func (m stashModel) selectedMarkdown() *markdown { // Adds markdown documents to the model. func (m *stashModel) addMarkdowns(mds ...*markdown) { if len(mds) > 0 { + for _, md := range mds { + md.generateLocalID() + } + m.markdowns = append(m.markdowns, mds...) if !m.isFiltering() { sort.Stable(markdownsByLocalFirst(m.markdowns)) @@ -340,7 +351,7 @@ func (m *stashModel) openMarkdown(md *markdown) tea.Cmd { if md.markdownType == LocalDoc { cmd = loadLocalMarkdown(md) } else { - cmd = loadRemoteMarkdown(m.general.cc, md.ID, md.markdownType) + cmd = loadRemoteMarkdown(m.general.cc, md) } return tea.Batch(cmd, spinner.Tick) @@ -462,7 +473,7 @@ func newStashModel(general *general) stashModel { serverPage: 1, loaded: NewDocTypeSet(), loadingFromNetwork: true, - filesStashed: make(map[string]struct{}), + filesStashed: make(map[ksuid.KSUID]struct{}), sections: s, } @@ -533,6 +544,14 @@ func (m stashModel) update(msg tea.Msg) (stashModel, tea.Cmd) { m.addMarkdowns(docs...) + case markdownFetchFailedMsg: + s := "Couldn't load markdown" + if msg.note != "" { + s += ": " + msg.note + } + cmd := m.newStatusMessage(s) + return m, cmd + case filteredMarkdownMsg: m.filteredMarkdowns = msg return m, nil @@ -714,23 +733,20 @@ func (m *stashModel) handleDocumentBrowsing(msg tea.Msg) tea.Cmd { md := m.selectedMarkdown() - if _, alreadyStashed := m.filesStashed[md.localPath]; alreadyStashed { + if _, alreadyStashed := m.filesStashed[md.localID]; alreadyStashed { cmds = append(cmds, m.newStatusMessage("Already stashed")) break } - isLocalMarkdown := md.markdownType == LocalDoc - markdownPathMissing := md.localPath == "" - - if !isLocalMarkdown || markdownPathMissing { - if debug && isLocalMarkdown && markdownPathMissing { - log.Printf("refusing to stash markdown; local path is empty: %#v", md) + 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) } break } // Checks passed; perform the stash - m.filesStashed[md.localPath] = struct{}{} + m.filesStashed[md.localID] = struct{}{} cmds = append(cmds, stashDocument(m.general.cc, *md)) if m.loadingDone() && !m.spinner.Visible() { @@ -807,7 +823,6 @@ func (m *stashModel) handleDocumentBrowsing(msg tea.Msg) tea.Cmd { func (m *stashModel) handleDeleteConfirmation(msg tea.Msg) tea.Cmd { if msg, ok := msg.(tea.KeyMsg); ok { switch msg.String() { - // Confirm deletion case "y": if m.selectionState != selectionPromptingDelete { break @@ -819,10 +834,8 @@ func (m *stashModel) handleDeleteConfirmation(msg tea.Msg) tea.Cmd { continue } - if md.markdownType == ConvertedDoc { - // Remove from the things-we-stashed-this-session set - delete(m.filesStashed, md.localPath) - } + // Remove from the things-we-stashed-this-session set + delete(m.filesStashed, md.localID) // Delete optimistically and remove the stashed item before // we've received a success response. @@ -976,7 +989,8 @@ func (m stashModel) view() string { // Rules for the logo, filter and status message. var logoOrFilter string if m.showStatusMessage { - logoOrFilter = greenFg(m.statusMessage) + const gutter = 3 + logoOrFilter = greenFg(truncate(m.statusMessage, m.general.width-gutter)) } else if m.isFiltering() { logoOrFilter = m.filterInput.View() } else { @@ -1184,31 +1198,47 @@ func (m stashModel) populatedView() string { // COMMANDS -func loadRemoteMarkdown(cc *charm.Client, id int, t DocType) tea.Cmd { +// loadRemoteMarkdown is a command for loading markdown from the server. +func loadRemoteMarkdown(cc *charm.Client, md *markdown) tea.Cmd { return func() tea.Msg { - var ( - md *charm.Markdown - err error - ) - - if t == StashedDoc || t == ConvertedDoc { - md, err = cc.GetStashMarkdown(id) - } else { - md, err = cc.GetNewsMarkdown(id) - } - + md, err := loadMarkdownFromCharm(cc, md.ID, md.markdownType) if err != nil { if debug { - log.Println("error loading remote markdown:", err) + 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, } - return errMsg{err} } + return fetchedMarkdownMsg(md) + } +} + +// loadMarkdownFromCharm performs the actual I/O for loading markdown from the +// sever. +func loadMarkdownFromCharm(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) + } - return fetchedMarkdownMsg(&markdown{ - markdownType: t, - Markdown: *md, - }) + if err != nil { + return nil, err } + + return &markdown{ + markdownType: t, + Markdown: *md, + }, nil } func loadLocalMarkdown(md *markdown) tea.Cmd { @@ -1287,7 +1317,7 @@ func deleteMarkdown(markdowns []*markdown, target *markdown) ([]*markdown, error index = i } default: - return nil, fmt.Errorf("%s documents cannot be deleted", target.markdownType.String()) + return nil, fmt.Errorf("%s documents cannot be deleted", target.markdownType) } } diff --git a/ui/ui.go b/ui/ui.go index 9160c30b..cf76d1a2 100644 --- a/ui/ui.go +++ b/ui/ui.go @@ -30,6 +30,8 @@ var ( config Config glowLogoTextColor = common.Color("#ECFD65") debug = false // true if we're logging to a file, in which case we'll log more stuff + + stashableDocTypes = NewDocTypeSet(LocalDoc, NewsDoc) ) // Config contains TUI-specific configuration. @@ -649,26 +651,40 @@ func stashDocument(cc *charm.Client, md markdown) tea.Cmd { } // Is the document missing a body? If so, it likely means it needs to - // be loaded. If the document body is really empty then we'll still - // stash it. + // be loaded. But...if it turnsout the document body really is empty + // then we'll stash it anyway. if len(md.Body) == 0 { - data, err := ioutil.ReadFile(md.localPath) - if err != nil { + switch md.markdownType { + + case LocalDoc: + data, err := ioutil.ReadFile(md.localPath) + if err != nil { + if debug { + log.Println("error loading document body for stashing:", err) + } + return stashErrMsg{err} + } + md.Body = string(data) + + case NewsDoc: + newMD, err := loadMarkdownFromCharm(cc, md.ID, md.markdownType) + if err != nil { + return stashErrMsg{err} + } + md.Body = newMD.Body + + default: if debug { - log.Println("error loading doucument body for stashing:", err) + log.Printf("user is attempting to stash an unsupported markdown type: %s", md.markdownType) } - return stashErrMsg{err} } - md.Body = string(data) } - // Turn local markdown into a newly stashed (converted) markdown - md.markdownType = ConvertedDoc - md.CreatedAt = time.Now() - // Set the note as the filename without the extension - p := md.localPath - md.Note = strings.Replace(path.Base(p), path.Ext(p), "", 1) + if md.markdownType == LocalDoc { + p := md.localPath + md.Note = strings.Replace(path.Base(p), path.Ext(p), "", 1) + } newMd, err := cc.StashMarkdown(md.Note, md.Body) if err != nil { @@ -678,9 +694,15 @@ func stashDocument(cc *charm.Client, md markdown) tea.Cmd { return stashErrMsg{err} } - // We really just need to know the ID so we can operate on this newly - // stashed markdown. + // The server sends the whole stashed document back, but we really just + // need to know the ID so we can operate on this newly stashed + // markdown. md.ID = newMd.ID + + // Turn the markdown into a newly stashed (converted) markdown + md.markdownType = ConvertedDoc + md.CreatedAt = time.Now() + return stashSuccessMsg(md) } } @@ -729,6 +751,9 @@ func indent(s string, n int) string { } func truncate(str string, num int) string { + if num < 1 { + return str + } return runewidth.Truncate(str, num, "…") }