mirror of
https://github.com/charmbracelet/glow
synced 2024-11-10 06:04:18 +00:00
Make news stashable
This commit is contained in:
parent
4fcf48f92a
commit
88806c8abc
5 changed files with 126 additions and 54 deletions
1
go.mod
1
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
|
||||
|
|
4
go.sum
4
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=
|
||||
|
|
|
@ -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)
|
||||
|
|
106
ui/stash.go
106
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,33 +1198,49 @@ 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(&markdown{
|
||||
markdownType: t,
|
||||
Markdown: *md,
|
||||
})
|
||||
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)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &markdown{
|
||||
markdownType: t,
|
||||
Markdown: *md,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func loadLocalMarkdown(md *markdown) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
if md.markdownType != LocalDoc {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
57
ui/ui.go
57
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 {
|
||||
if debug {
|
||||
log.Println("error loading doucument body for stashing:", err)
|
||||
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.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, "…")
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue