diff --git a/ui/pager.go b/ui/pager.go index df86a2e..a47b552 100644 --- a/ui/pager.go +++ b/ui/pager.go @@ -5,7 +5,9 @@ import ( "fmt" "log" "math" + "path" "strings" + "time" "github.com/charmbracelet/bubbles/textinput" "github.com/charmbracelet/bubbles/viewport" @@ -24,6 +26,8 @@ const ( gray = "#333333" yellowGreen = "#ECFD65" fuschia = "#EE6FF8" + mintGreen = "#89F0CB" + darkGreen = "#089B66" noteHeadingText = " Set Memo " notePromptText = " > " ) @@ -40,31 +44,36 @@ var ( statusBarNoteFg = common.NewColorPair("#7D7D7D", "#656565") statusBarScrollPosFg = common.NewColorPair("#5A5A5A", "#949494") - statusBarScrollPosStyle = te.Style{}. - Foreground(statusBarScrollPosFg.Color()). - Background(statusBarBg.Color()). - Styled - - statusBarNoteStyle = te.Style{}. - Foreground(statusBarNoteFg.Color()). - Background(statusBarBg.Color()). - Styled - - statusBarHelpStyle = te.Style{}. - Foreground(statusBarNoteFg.Color()). - Background(common.NewColorPair("#323232", "#DCDCDC").Color()). - Styled - - helpViewStyle = te.Style{}. - Foreground(statusBarNoteFg.Color()). - Background(common.NewColorPair("#1B1B1B", "#f2f2f2").Color()). - Styled + // Styling functions + statusBarScrollPosStyle = newStyle(statusBarScrollPosFg.String(), statusBarBg.String()) + statusBarNoteStyle = newStyle(statusBarNoteFg.String(), statusBarBg.String()) + statusBarHelpStyle = newStyle(statusBarNoteFg.String(), common.NewColorPair("#323232", "#DCDCDC").String()) + statusBarMessageHeaderStyle = newStyle(common.Cream.String(), common.Green.String()) + statusBarMessageBodyStyle = newStyle(mintGreen, darkGreen) + helpViewStyle = newStyle(statusBarNoteFg.String(), common.NewColorPair("#1B1B1B", "#f2f2f2").String()) ) +// Create a new termenv styling function +func newStyle(fg, bg string) func(string) string { + return te.Style{}. + Foreground(te.ColorProfile().Color(fg)). + Background(te.ColorProfile().Color(bg)). + Styled +} + // MSG type contentRenderedMsg string type noteSavedMsg *charm.Markdown +type statusMessageTimeoutMsg struct{} +type stashSuccessMsg markdown +type stashErrMsg struct { + err error +} + +func (s stashErrMsg) Error() string { + return s.err.Error() +} // MODEL @@ -73,6 +82,8 @@ type pagerState int const ( pagerStateBrowse pagerState = iota pagerStateSetNote + pagerStateStashing + pagerStateStatusMessage ) type pagerModel struct { @@ -85,9 +96,13 @@ type pagerModel struct { textInput textinput.Model showHelp bool + statusMessageHeader string + statusMessageBody string + statusMessageTimer *time.Timer + // Current document being rendered, sans-glamour rendering. We cache // it here so we can re-render it on resize. - currentDocument *markdown + currentDocument markdown } func newPagerModel(glamourStyle string) pagerModel { @@ -145,6 +160,9 @@ func (m *pagerModel) unload() { if m.showHelp { m.toggleHelp() } + if m.statusMessageTimer != nil { + m.statusMessageTimer.Stop() + } m.state = pagerStateBrowse m.viewport.SetContent("") m.viewport.YOffset = 0 @@ -192,12 +210,38 @@ func pagerUpdate(msg tea.Msg, m pagerModel) (pagerModel, tea.Cmd) { } m.state = pagerStateSetNote + + // Stop the timer for hiding a status message since changing + // the state above will have cleared it. + if m.statusMessageTimer != nil { + m.statusMessageTimer.Stop() + } + + // Pre-populate note with existing value if m.textInput.Value() == "" { - // Pre-populate note with existing value m.textInput.SetValue(m.currentDocument.Note) m.textInput.CursorEnd() } + return m, textinput.Blink(m.textInput) + case "s": + // Stash a local document + if m.state != pagerStateStashing && m.currentDocument.markdownType == localMarkdown { + m.state = pagerStateStashing + + // Create new markdown based on loaded markdown + md := m.currentDocument + md.markdownType = stashedMarkdown + t := time.Now() + md.CreatedAt = &t + + // Set the note as the filename without the extension + p := md.localPath + md.Note = strings.Replace(path.Base(p), path.Ext(p), "", 1) + md.localPath = "" + + return m, stashDocument(m.cc, md) + } case "?": m.toggleHelp() if m.viewport.HighPerformanceRendering { @@ -216,11 +260,33 @@ func pagerUpdate(msg tea.Msg, m pagerModel) (pagerModel, tea.Cmd) { // We've reveived terminal dimensions, either for the first time or // after a resize case tea.WindowSizeMsg: - var cmd tea.Cmd - if m.currentDocument != nil { - cmd = renderWithGlamour(m, m.currentDocument.Body) - } - return m, cmd + return m, renderWithGlamour(m, m.currentDocument.Body) + + case stashSuccessMsg: + // Stashing was successful. Convert the loaded document to a stashed + // one. Note that we're also handling this message in the main update + // function where we're adding this stashed item to the stash listing. + + m.state = pagerStateBrowse + + // Replace the current document in the state so its metadata becomes + // that of a stashed document, but don't re-render since the body is + // identical to what's already rendered. + m.currentDocument = markdown(msg) + + // Show a success message to the user. + m.state = pagerStateStatusMessage + m.statusMessageHeader = "Stashed!" + m.statusMessageBody = "" + m.statusMessageTimer = time.NewTimer(time.Second * 3) + cmds = append(cmds, waitForStatusMessageTimeout(m.statusMessageTimer)) + + case stashErrMsg: + // TODO + + case statusMessageTimeoutMsg: + // Hide the status message bar + m.state = pagerStateBrowse } switch m.state { @@ -243,9 +309,12 @@ func pagerView(m pagerModel) string { fmt.Fprint(&b, viewport.View(m.viewport)+"\n") // Footer - if m.state == pagerStateSetNote { + switch m.state { + case pagerStateSetNote: pagerSetNoteView(&b, m) - } else { + case pagerStateStatusMessage: + pagerStatusMessageView(&b, m) + default: pagerStatusBarView(&b, m) } @@ -304,16 +373,37 @@ func pagerSetNoteView(b *strings.Builder, m pagerModel) { fmt.Fprint(b, textinput.View(m.textInput)) } +func pagerStatusMessageView(b *strings.Builder, m pagerModel) { + const bodyGapWidth = 2 // extra spaces we're adding before/after the body + + header := m.statusMessageHeader + if len(header) > 0 { + header = " " + header + " " + } + body := m.statusMessageBody + bodyWidth := runewidth.StringWidth(body) + availBodySpace := m.width - runewidth.StringWidth(header) - bodyGapWidth + + if availBodySpace < runewidth.StringWidth(body) { + body = runewidth.Truncate(body, availBodySpace, "…") + } else if availBodySpace > bodyWidth { + body = " " + body + " " + strings.Repeat(" ", availBodySpace-bodyWidth) + } + + if len(header) > 0 { + fmt.Fprintf(b, statusBarMessageHeaderStyle(header)) + } + fmt.Fprintf(b, statusBarMessageBodyStyle(body)) +} + func pagerHelpView(m pagerModel, width int) (s string) { col1 := [...]string{ "m set memo", "esc back to files", "q quit", } - if m.currentDocument != nil && m.currentDocument.markdownType != stashedMarkdown { - col1[0] = col1[1] - col1[1] = col1[2] - col1[2] = "" + if m.currentDocument.markdownType != stashedMarkdown { + col1[0] = "s stash this document" } s += "\n" @@ -374,6 +464,35 @@ func saveDocumentNote(cc *charm.Client, id int, note string) tea.Cmd { } } +func stashDocument(cc *charm.Client, md markdown) tea.Cmd { + if cc == nil { + return func() tea.Msg { + err := errors.New("can't stash; no charm client") + if debug { + log.Println("error stash document:", err) + } + return stashErrMsg{err} + } + } + return func() tea.Msg { + err := cc.StashMarkdown(md.Note, md.Body) + if err != nil { + if debug { + log.Println("error stashing document:", err) + } + return errMsg(err) + } + return stashSuccessMsg(md) + } +} + +func waitForStatusMessageTimeout(t *time.Timer) tea.Cmd { + return func() tea.Msg { + <-t.C + return statusMessageTimeoutMsg{} + } +} + // This is where the magic happens func glamourRender(m pagerModel, markdown string) (string, error) { diff --git a/ui/stash.go b/ui/stash.go index 11f06a6..3d9a731 100644 --- a/ui/stash.go +++ b/ui/stash.go @@ -58,7 +58,7 @@ const ( type markdown struct { markdownType markdownType localPath string // only relevent to local files - *charm.Markdown + charm.Markdown } // Sort documents with local files first, then by date @@ -705,7 +705,7 @@ func loadRemoteMarkdown(cc *charm.Client, id int, t markdownType) tea.Cmd { return fetchedMarkdownMsg(&markdown{ markdownType: t, - Markdown: md, + Markdown: *md, }) } } @@ -749,7 +749,7 @@ func wrapMarkdowns(t markdownType, md []*charm.Markdown) (m []*markdown) { for _, v := range md { m = append(m, &markdown{ markdownType: t, - Markdown: v, + Markdown: *v, }) } return m diff --git a/ui/ui.go b/ui/ui.go index 69e8e21..4f1dee2 100644 --- a/ui/ui.go +++ b/ui/ui.go @@ -236,7 +236,7 @@ func update(msg tea.Msg, mdl tea.Model) (tea.Model, tea.Cmd) { return m, tea.Quit case "left", "h", "delete": - if m.state == stateShowDocument && m.pager.state == pagerStateBrowse { + if m.state == stateShowDocument && (m.pager.state == pagerStateBrowse || m.pager.state == pagerStateStatusMessage) { cmds = append(cmds, m.unloadDocument()...) return m, tea.Batch(cmds...) } @@ -310,7 +310,7 @@ func update(msg tea.Msg, mdl tea.Model) (tea.Model, tea.Cmd) { cmds = append(cmds, loadStash(m.stash), loadNews(m.stash)) case fetchedMarkdownMsg: - m.pager.currentDocument = msg + m.pager.currentDocument = *msg cmds = append(cmds, renderWithGlamour(m.pager, msg.Body)) case contentRenderedMsg: @@ -331,6 +331,12 @@ func update(msg tea.Msg, mdl tea.Model) (tea.Model, tea.Cmd) { m.stash = stashModel return m, cmd + case stashSuccessMsg: + // Something was stashed. Update the stash listing. We're caching this + // here, rather than in the stash update function, because stashing + // may have occurred in the pager. + md := markdown(msg) + m.stash.addMarkdowns(&md) } // Process children @@ -497,7 +503,7 @@ func localFileToMarkdown(cwd string, res gitcha.SearchResult) (*markdown, error) md := &markdown{ markdownType: localMarkdown, localPath: res.Path, - Markdown: &charm.Markdown{}, + Markdown: charm.Markdown{}, } // Strip absolute path