Skip to content

Commit

Permalink
Make news stashable
Browse files Browse the repository at this point in the history
  • Loading branch information
meowgorithm committed Dec 24, 2020
1 parent 4fcf48f commit 88806c8
Show file tree
Hide file tree
Showing 5 changed files with 124 additions and 52 deletions.
1 change: 1 addition & 0 deletions go.mod
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Expand Up @@ -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=
Expand Down
12 changes: 12 additions & 0 deletions ui/markdown.go
Expand Up @@ -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"
Expand All @@ -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
Expand All @@ -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)
Expand Down
104 changes: 67 additions & 37 deletions ui/stash.go
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/muesli/reflow/ansi"
te "github.com/muesli/termenv"
"github.com/sahilm/fuzzy"
"github.com/segmentio/ksuid"
)

const (
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
}

Expand Down
55 changes: 40 additions & 15 deletions ui/ui.go
Expand Up @@ -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.
Expand Down Expand Up @@ -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 {
Expand All @@ -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)
}
}
Expand Down Expand Up @@ -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, "…")
}

Expand Down

0 comments on commit 88806c8

Please sign in to comment.