From eba22f08b14ec308ad295b785f37bfb2e7f4abe9 Mon Sep 17 00:00:00 2001 From: Christian Rocha Date: Thu, 21 May 2020 22:29:46 -0400 Subject: [PATCH] Also set/edit notes in stash view --- ui/pager.go | 2 +- ui/stash.go | 190 ++++++++++++++++++++++++++++++++++++++-------------- ui/ui.go | 28 +++++++- 3 files changed, 166 insertions(+), 54 deletions(-) diff --git a/ui/pager.go b/ui/pager.go index 9ed844d..ecd53c9 100644 --- a/ui/pager.go +++ b/ui/pager.go @@ -76,7 +76,7 @@ func newPagerModel(glamourStyle string) pagerModel { ti.TextColor = gray ti.BackgroundColor = yellowGreen ti.CursorColor = fuschia - ti.CharLimit = 128 // totally arbitrary + ti.CharLimit = noteCharacterLimit ti.Focus() return pagerModel{ diff --git a/ui/stash.go b/ui/stash.go index e1d9c9b..76bc399 100644 --- a/ui/stash.go +++ b/ui/stash.go @@ -11,6 +11,7 @@ import ( "github.com/charmbracelet/boba" "github.com/charmbracelet/boba/paginator" "github.com/charmbracelet/boba/spinner" + "github.com/charmbracelet/boba/textinput" "github.com/charmbracelet/charm" "github.com/charmbracelet/charm/ui/common" "github.com/dustin/go-humanize" @@ -19,15 +20,18 @@ import ( ) const ( - stashViewItemHeight = 3 - stashViewTopPadding = 5 - stashViewBottomPadding = 4 - stashHorizontalPadding = 6 + stashViewItemHeight = 3 + stashViewTopPadding = 5 + stashViewBottomPadding = 4 + stashViewHorizontalPadding = 6 + setNotePromptText = "Memo: " ) var ( - faintGreen = common.NewColorPair("#2B4A3F", "#ABE5D1") - green = common.NewColorPair("#04B575", "#04B575") + faintGreen = common.NewColorPair("#2B4A3F", "#ABE5D1") + green = common.NewColorPair("#04B575", "#04B575") + dullYellow = common.NewColorPair("#9BA92F", "#6CCCA9") // renders light green on light backgrounds + dullFuchsia = common.NewColorPair("#AD58B4", "#F9ACFF") ) // MSG @@ -41,6 +45,8 @@ type deletedStashedItemMsg int // MODEL +// markdownType allows us to differentiate between the types of markdown +// documents we're dealing with, namely stuff the user stashed versus news. type markdownType int const ( @@ -49,7 +55,7 @@ const ( ) // markdown wraps charm.Markdown so we can differentiate between stashed items -// and news +// and news. type markdown struct { markdownType markdownType *charm.Markdown @@ -70,6 +76,7 @@ const ( stashStateReady stashStatePromptDelete stashStateLoadingDocument + stashStateSettingNote ) type stashLoadedState byte @@ -89,6 +96,7 @@ type stashModel struct { state stashState documents []*markdown spinner spinner.Model + noteInput textinput.Model terminalWidth int terminalHeight int loaded stashLoadedState // what's loaded? we find out with bitmasking @@ -120,6 +128,8 @@ func (m *stashModel) setSize(width, height int) { m.paginator.PerPage = perPage m.paginator.SetTotalPages(len(m.documents)) + m.noteInput.Width = m.terminalWidth - stashViewHorizontalPadding*2 - len(setNotePromptText) + // Make sure the page stays in bounds if m.paginator.Page >= m.paginator.TotalPages-1 { m.paginator.Page = max(0, m.paginator.TotalPages-1) @@ -143,9 +153,16 @@ func stashInit(cc *charm.Client) (stashModel, boba.Cmd) { p.Type = paginator.Dots p.InactiveDot = common.Subtle("•") + ni := textinput.NewModel() + ni.Prompt = te.String(setNotePromptText).Foreground(common.YellowGreen.Color()).String() + ni.CursorColor = common.Fuschia.String() + ni.CharLimit = noteCharacterLimit // totally arbitrary + ni.Focus() + m := stashModel{ cc: cc, spinner: s, + noteInput: ni, page: 1, paginator: p, } @@ -166,11 +183,11 @@ func stashUpdate(msg boba.Msg, m stashModel) (stashModel, boba.Cmd) { ) switch msg := msg.(type) { - case boba.KeyMsg: - // Don't respond to keystrokes if we're still loading - if m.state == stashStateInit { - return m, nil + // Don't respond to these keystrokes if we're still loading or setting + // a note + if m.state == stashStateInit || m.state == stashStateSettingNote { + break } switch msg.String() { @@ -212,12 +229,24 @@ func stashUpdate(msg boba.Msg, m stashModel) (stashModel, boba.Cmd) { spinner.Tick(m.spinner), ) + // Set note + case "n": + if m.state != stashStateSettingNote && m.state != stashStatePromptDelete { + m.state = stashStateSettingNote + m.noteInput.SetValue(m.documents[m.markdownIndex()].Note) + m.noteInput.CursorEnd() + return m, textinput.Blink(m.noteInput) + } + // Confirm deletion case "x": - if m.documents[m.markdownIndex()].markdownType == userMarkdown { + isUserMarkdown := m.documents[m.markdownIndex()].markdownType == userMarkdown + isValidState := m.state != stashStateSettingNote + if isUserMarkdown && isValidState { m.state = stashStatePromptDelete } + // Deletion confirmed case "y": if m.state != stashStatePromptDelete { break @@ -285,8 +314,8 @@ func stashUpdate(msg boba.Msg, m stashModel) (stashModel, boba.Cmd) { cmds = append(cmds, cmd) } - // A note was set on a document. This happened in the pager, so we'll - // find the corresponding document here and update accordingly. + // A note was set on a document. This may have happened in the pager, so + // we'll find the corresponding document here and update accordingly. case noteSavedMsg: for i := range m.documents { if m.documents[i].ID == msg.ID { @@ -295,7 +324,8 @@ func stashUpdate(msg boba.Msg, m stashModel) (stashModel, boba.Cmd) { } } - if m.state == stashStateReady { + switch m.state { + case stashStateReady: // Update paginator m.paginator, cmd = paginator.Update(msg, m.paginator) @@ -315,6 +345,32 @@ func stashUpdate(msg boba.Msg, m stashModel) (stashModel, boba.Cmd) { cmds = append(cmds, loadStash(m)) } + case stashStateSettingNote: + + if msg, ok := msg.(boba.KeyMsg); ok { + switch msg.String() { + case "q": + fallthrough + case "esc": + // Cancel note + m.state = stashStateReady + m.noteInput.Reset() + case "enter": + // Set new note + doc := m.documents[m.markdownIndex()] + newNote := m.noteInput.Value() + cmd = saveDocumentNote(m.cc, doc.ID, newNote) + doc.Note = newNote + m.noteInput.Reset() + m.state = stashStateReady + return m, cmd + } + } + + // Update the text input component used to set notes + m.noteInput, cmd = textinput.Update(msg, m.noteInput) + cmds = append(cmds, cmd) + } // If an item is being confirmed for delete, any key (other than the key @@ -342,6 +398,8 @@ func stashView(m stashModel) string { s += spinner.View(m.spinner) + " Loading document..." case stashStateReady: fallthrough + case stashStateSettingNote: + fallthrough case stashStatePromptDelete: if len(m.documents) == 0 { s += stashEmtpyView(m) @@ -356,12 +414,13 @@ func stashView(m stashModel) string { blankLines = strings.Repeat("\n", numBlankLines) } - var header string - if m.state == stashStatePromptDelete { + header := "Here’s your markdown stash:" + switch m.state { + case stashStatePromptDelete: header = te.String("Delete this item? ").Foreground(common.Red.Color()).String() + te.String("(y/N)").Foreground(common.FaintRed.Color()).String() - } else { - header = "Here’s your markdown stash:" + case stashStateSettingNote: + header = te.String("Set the memo for this item?").Foreground(common.YellowGreen.Color()).String() } var pagination string @@ -405,13 +464,18 @@ func stashPopulatedView(m stashModel) string { docs := m.documents[start:end] for i, v := range docs { - state := common.StateNormal - if i == m.index && m.state == stashStatePromptDelete { - state = common.StateDeleting - } else if i == m.index { - state = common.StateSelected + state := markdownStateNormal + if i == m.index { + switch m.state { + case stashStatePromptDelete: + state = markdownStateDeleting + case stashStateSettingNote: + state = markdownStateSettingNote + default: + state = markdownStateSelected + } } - s += stashListItemView(*v).render(m.terminalWidth, state) + "\n\n" + s += stashListItemView(*v).render(m, state) + "\n\n" } s = strings.TrimSpace(s) // trim final newlines @@ -433,7 +497,9 @@ func stashHelpView(m stashModel) string { isNews bool = m.documents[m.markdownIndex()].markdownType == newsMarkdown ) - if m.state == stashStatePromptDelete { + if m.state == stashStateSettingNote { + h = append(h, "enter: confirm", "esc: cancel") + } else if m.state == stashStatePromptDelete { h = append(h, "y: delete", "n: cancel") } else { h = append(h, "enter: open") @@ -451,25 +517,36 @@ func stashHelpView(m stashModel) string { return common.HelpView(h...) } -// stashListItemView renders items in the stash view +// markdownState is used in a deterministic fashion to aid rendering stash item +// views. +type markdownState int + +const ( + markdownStateNormal markdownState = iota + markdownStateSelected + markdownStateDeleting + markdownStateSettingNote +) + +// stashListItemView contains methods for rendering an item as it appears in +// the stash view type stashListItemView markdown -func (m stashListItemView) render(width int, state common.State) string { - - var line string - if m.markdownType == newsMarkdown && state != common.StateSelected { - line = te.String("│").Foreground(faintGreen.Color()).String() - } else { - line = common.VerticalLine(state) - } - line += " " +func (m stashListItemView) render(mdl stashModel, state markdownState) string { + // General key color keyColor := common.NoColor + line := common.VerticalLine(common.StateNormal) switch state { - case common.StateSelected: + case markdownStateSettingNote: + keyColor = common.YellowGreen + line = common.VerticalLine(common.StateActive) + case markdownStateSelected: keyColor = common.Fuschia - case common.StateDeleting: + line = common.VerticalLine(common.StateSelected) + case markdownStateDeleting: keyColor = common.Red + line = common.VerticalLine(common.StateDeleting) default: if m.markdownType == newsMarkdown { keyColor = common.Green @@ -477,11 +554,15 @@ func (m stashListItemView) render(width int, state common.State) string { } titleKey := "#" + strconv.Itoa(m.ID) - if m.markdownType == newsMarkdown { - titleKey = "System Announcement" - } - if m.Note != "" { - titleKey += ":" + if state == markdownStateSettingNote { + titleKey = textinput.View(mdl.noteInput) + } else { + if m.markdownType == newsMarkdown { + titleKey = "System Announcement" + } + if m.Note != "" { + titleKey += ":" + } } dateKey := "Stashed:" @@ -489,34 +570,39 @@ func (m stashListItemView) render(width int, state common.State) string { dateKey = "Posted:" } - titleVal := truncate(m.Note, width-(stashHorizontalPadding*2)-len(titleKey)) - titleVal = m.title(titleVal, state) + var titleVal string + if state != markdownStateSettingNote { + titleVal = truncate(m.Note, mdl.terminalWidth-(stashViewHorizontalPadding*2)-len(titleKey)) + titleVal = m.title(titleVal, state) + } titleKey = te.String(titleKey).Foreground(keyColor.Color()).String() dateKey = te.String(dateKey).Foreground(keyColor.Color()).String() dateVal := m.date(state) var s string - s += fmt.Sprintf("%s%s %s\n", line, titleKey, titleVal) - s += fmt.Sprintf("%s%s %s", line, dateKey, dateVal) + s += fmt.Sprintf("%s %s %s\n", line, titleKey, titleVal) + s += fmt.Sprintf("%s %s %s", line, dateKey, dateVal) return s } -func (m stashListItemView) date(state common.State) string { +func (m stashListItemView) date(state markdownState) string { c := common.Indigo - if state == common.StateDeleting { + if state == markdownStateDeleting { c = common.FaintRed + } else if state == markdownStateSettingNote { + c = dullYellow } s := relativeTime(*m.CreatedAt) return te.String(s).Foreground(c.Color()).String() } -func (m stashListItemView) title(title string, state common.State) string { +func (m stashListItemView) title(title string, state markdownState) string { if title == "" { return "" } c := common.Indigo - if state == common.StateDeleting { + if state == markdownStateDeleting { c = common.Red } return te.String(title).Foreground(c.Color()).String() @@ -569,6 +655,8 @@ func deleteStashedItem(cc *charm.Client, id int) boba.Cmd { // ETC +// wrapMarkdowns wraps a *charm.Markdown with a *markdown in order to add some +// extra metadata. func wrapMarkdowns(t markdownType, md []*charm.Markdown) (m []*markdown) { for _, v := range md { m = append(m, &markdown{ diff --git a/ui/ui.go b/ui/ui.go index 82d494f..f990203 100644 --- a/ui/ui.go +++ b/ui/ui.go @@ -12,6 +12,10 @@ import ( te "github.com/muesli/termenv" ) +const ( + noteCharacterLimit = 128 // totally arbitrary +) + var ( glowLogoTextColor = common.Color("#ECFD65") ) @@ -50,6 +54,18 @@ const ( stateShowDocument ) +// Stringn translates the staus to a human-readable string. This is just for +// debugging. +func (s state) String() string { + return [...]string{ + "initializing", + "running keygen", + "keygen finished", + "showing stash", + "showing document", + }[s] +} + type model struct { cc *charm.Client user *charm.User @@ -119,8 +135,15 @@ func update(msg boba.Msg, mdl boba.Model) (boba.Model, boba.Cmd) { case "q": fallthrough case "esc": - if m.state == stateShowDocument { - var cmd boba.Cmd + var cmd boba.Cmd + + switch m.state { + case stateShowStash: + if m.stash.state == stashStateSettingNote { + m.stash, cmd = stashUpdate(msg, m.stash) + return m, cmd + } + case stateShowDocument: if m.pager.state == pagerStateBrowse { // Exit pager m.unloadDocument() @@ -130,6 +153,7 @@ func update(msg boba.Msg, mdl boba.Model) (boba.Model, boba.Cmd) { } return m, cmd } + return m, boba.Quit case "ctrl+c":