diff --git a/go.mod b/go.mod index 164f3c0..6ce5fca 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/charmbracelet/glow go 1.13 require ( - github.com/charmbracelet/boba v0.6.0 + github.com/charmbracelet/boba v0.6.1 github.com/charmbracelet/charm v0.4.1 github.com/charmbracelet/glamour v0.2.0 github.com/charmbracelet/tea v0.3.0 @@ -12,8 +12,9 @@ require ( github.com/dustin/go-humanize v1.0.1-0.20200219035652-afde56e7acac github.com/muesli/reflow v0.1.0 github.com/muesli/termenv v0.6.0 + github.com/pkg/term v0.0.0-20200520122047-c3ffed290a03 // indirect github.com/spf13/cobra v0.0.7 golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37 golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 // indirect - golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9 + golang.org/x/sys v0.0.0-20200519105757-fe76b779f299 ) diff --git a/go.sum b/go.sum index 797a152..7d84bde 100644 --- a/go.sum +++ b/go.sum @@ -16,8 +16,8 @@ github.com/calmh/randomart v1.1.0 h1:evl+iwc10LXtHdMZhzLxmsCQVmWnkXs44SbC6Uk0Il8 github.com/calmh/randomart v1.1.0/go.mod h1:DQUbPVyP+7PAs21w/AnfMKG5NioxS3TbZ2F9MSK/jFM= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/charmbracelet/boba v0.5.0/go.mod h1:xqhji08CbPctzbvCi/GzmNvIFOhnWJnkl9N8T65gnK0= -github.com/charmbracelet/boba v0.6.0 h1:ktCY611UE4ZJ4MS0FS6eCA/jqjJDyfNgFfdDKBgi07g= -github.com/charmbracelet/boba v0.6.0/go.mod h1:xqhji08CbPctzbvCi/GzmNvIFOhnWJnkl9N8T65gnK0= +github.com/charmbracelet/boba v0.6.1 h1:UwhxBYkYjnRTS8oJBoLxXzh+CpE12yt7utMpP0cjlYc= +github.com/charmbracelet/boba v0.6.1/go.mod h1:xqhji08CbPctzbvCi/GzmNvIFOhnWJnkl9N8T65gnK0= github.com/charmbracelet/charm v0.4.1 h1:LJjQUDjeJkMTWmovyYiUzglbbbMBQGoDomqBn22qbpI= github.com/charmbracelet/charm v0.4.1/go.mod h1:bZQON+c4uV9mgbZBvKzH7wA1zGII9Zfg26cL70NYdH0= github.com/charmbracelet/glamour v0.2.0 h1:mTgaiNiumpqTZp3qVM6DH9UB0NlbY17wejoMf1kM8Pg= @@ -106,8 +106,9 @@ github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/9 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/term v0.0.0-20190109203006-aa71e9d9e942 h1:A7GG7zcGjl3jqAqGPmcNjd/D9hzL95SuoOQAaFNdLU0= github.com/pkg/term v0.0.0-20190109203006-aa71e9d9e942/go.mod h1:eCbImbZ95eXtAUIbLAuAVnBnwf83mjf6QIVH8SHYwqQ= +github.com/pkg/term v0.0.0-20200520122047-c3ffed290a03 h1:pd4YKIqCB0U7O2I4gWHgEUA2mCEOENmco0l/bM957bU= +github.com/pkg/term v0.0.0-20200520122047-c3ffed290a03/go.mod h1:Z9+Ul5bCbBKnbCvdOWbLqTHhJiYV414CURZJba6L8qA= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= @@ -178,8 +179,8 @@ golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200430202703-d923437fa56d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200501145240-bc7a7d42d5c3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9 h1:YTzHMGlqJu67/uEo1lBv0n3wBXhXNeUbB1XfN2vmTm0= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299 h1:DYfZAGf2WMFjMxbgTjaC+2HC7NkNAQs+6Q8b9WEB/F4= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/ui/pager.go b/ui/pager.go index 7d498f1..4bd88c9 100644 --- a/ui/pager.go +++ b/ui/pager.go @@ -1,48 +1,92 @@ package ui import ( + "errors" "fmt" "math" "os" "strings" "github.com/charmbracelet/boba" + "github.com/charmbracelet/boba/textinput" "github.com/charmbracelet/boba/viewport" "github.com/charmbracelet/charm" + "github.com/charmbracelet/charm/ui/common" "github.com/charmbracelet/glamour" te "github.com/muesli/termenv" ) +const ( + statusBarHeight = 1 + gray = "#333333" + yellowGreen = "#ECFD65" + fuschia = "#EE6FF8" + noteHeadingText = " Set Memo " + notePromptText = " > " +) + +var ( + noteHeading = te.String(noteHeadingText). + Foreground(common.Cream.Color()). + Background(common.Green.Color()). + String() +) + // MSG +type pagerErrMsg error type contentRenderedMsg string +type noteSavedMsg *charm.Markdown // MODEL type pagerState int const ( - pagerStateNormal pagerState = iota + pagerStateBrowse pagerState = iota pagerStateSetNote ) type pagerModel struct { err error + cc *charm.Client viewport viewport.Model + state pagerState glamourStyle string width int height int + textInput textinput.Model // Current document being rendered, sans-glamour rendering. We cache // this here so we can re-render it on resize. currentDocument *charm.Markdown } +func newPagerModel(glamourStyle string) pagerModel { + ti := textinput.NewModel() + ti.Prompt = te.String(notePromptText). + Foreground(te.ColorProfile().Color(gray)). + Background(te.ColorProfile().Color(yellowGreen)). + String() + ti.TextColor = gray + ti.BackgroundColor = yellowGreen + ti.CursorColor = fuschia + ti.CharLimit = 128 // totally arbitrary + ti.Focus() + + return pagerModel{ + state: pagerStateBrowse, + glamourStyle: glamourStyle, + textInput: ti, + } +} + func (m *pagerModel) setSize(w, h int) { m.width = w m.height = h m.viewport.Width = w - m.viewport.Height = h + m.viewport.Height = h - statusBarHeight + m.textInput.Width = w - len(noteHeadingText) - len(notePromptText) - 1 } func (m *pagerModel) setContent(s string) { @@ -50,15 +94,55 @@ func (m *pagerModel) setContent(s string) { } func (m *pagerModel) unload() { + m.state = pagerStateBrowse m.viewport.SetContent("") m.viewport.Y = 0 + m.textInput.Reset() } // UPDATE func pagerUpdate(msg boba.Msg, m pagerModel) (pagerModel, boba.Cmd) { + var ( + cmd boba.Cmd + cmds []boba.Cmd + ) + switch msg := msg.(type) { + case boba.KeyMsg: + switch m.state { + case pagerStateSetNote: + switch msg.String() { + case "q": + fallthrough + case "esc": + m.state = pagerStateBrowse + return m, nil + case "enter": + m.currentDocument.Note = m.textInput.Value // set optimistically + m.state = pagerStateBrowse + m.textInput.Reset() + return m, saveDocumentNote(m.cc, m.currentDocument.ID, m.currentDocument.Note) + } + default: + switch msg.String() { + case "q": + fallthrough + case "esc": + if m.state != pagerStateBrowse { + m.state = pagerStateBrowse + return m, nil + } + case "n": + m.state = pagerStateSetNote + return m, textinput.Blink(m.textInput) + } + } + + case pagerErrMsg: + m.err = msg + // Glow has rendered the content case contentRenderedMsg: m.setContent(string(msg)) @@ -79,19 +163,32 @@ func pagerUpdate(msg boba.Msg, m pagerModel) (pagerModel, boba.Cmd) { return m, cmd } - var cmd boba.Cmd - m.viewport, cmd = viewport.Update(msg, m.viewport) + switch m.state { + case pagerStateSetNote: + m.textInput, cmd = textinput.Update(msg, m.textInput) + cmds = append(cmds, cmd) + default: + m.viewport, cmd = viewport.Update(msg, m.viewport) + cmds = append(cmds, cmd) + } - return m, cmd + return m, boba.Batch(cmds...) } // VIEW func pagerView(m pagerModel) string { + var footer string + if m.state == pagerStateSetNote { + footer = pagerSetNoteView(m) + } else { + footer = pagerStatusBarView(m) + } + return fmt.Sprintf( "\n%s\n%s", viewport.View(m.viewport), - pagerStatusBarView(m), + footer, ) } @@ -126,6 +223,10 @@ func pagerStatusBarView(m pagerModel) string { return logo + note + emptySpace + percent } +func pagerSetNoteView(m pagerModel) string { + return noteHeading + textinput.View(m.textInput) +} + // CMD func renderWithGlamour(m pagerModel, md string) boba.Cmd { @@ -138,6 +239,20 @@ func renderWithGlamour(m pagerModel, md string) boba.Cmd { } } +func saveDocumentNote(cc *charm.Client, id int, note string) boba.Cmd { + if cc == nil { + return func() boba.Msg { + return pagerErrMsg(errors.New("can't set note; no charm client")) + } + } + return func() boba.Msg { + if err := cc.SetMarkdownNote(id, note); err != nil { + return pagerErrMsg(err) + } + return noteSavedMsg(&charm.Markdown{ID: id, Note: note}) + } +} + // 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 918ac1d..f1a0292 100644 --- a/ui/stash.go +++ b/ui/stash.go @@ -39,7 +39,7 @@ type stashState int const ( stashStateInit stashState = iota - stashStateStashLoaded + stashStateReady stashStatePromptDelete stashStateLoadingDocument ) @@ -170,7 +170,7 @@ func stashUpdate(msg boba.Msg, m stashModel) (stashModel, boba.Cmd) { if m.state != stashStatePromptDelete { break } - // Deletion confirmed. Delete the stashed item. + // Deletion confirmed. Delete the stashed item... // Index of the documents slice we'll be deleting i := m.paginator.Page*m.paginator.PerPage + m.index @@ -187,9 +187,8 @@ func stashUpdate(msg boba.Msg, m stashModel) (stashModel, boba.Cmd) { m.paginator.Page = min(m.paginator.Page, m.paginator.TotalPages-1) // Set state and delete - m.state = stashStateStashLoaded + m.state = stashStateReady return m, deleteStashedItem(m.cc, id) - } case stashErrMsg: @@ -207,7 +206,7 @@ func stashUpdate(msg boba.Msg, m stashModel) (stashModel, boba.Cmd) { sort.Sort(charm.MarkdownsByCreatedAtDesc(msg)) // sort by date m.documents = append(m.documents, msg...) - m.state = stashStateStashLoaded + m.state = stashStateReady m.paginator.SetTotalPages(len(m.documents)) case stashSpinnerTickMsg: @@ -215,9 +214,18 @@ func stashUpdate(msg boba.Msg, m stashModel) (stashModel, boba.Cmd) { m.spinner, cmd = spinner.Update(msg, m.spinner) cmds = append(cmds, cmd) } + + case noteSavedMsg: + // A note was set on a document. This happened in the pager, so we'll + // find the corresponding document here and update accordingly. + for i := range m.documents { + if m.documents[i].ID == msg.ID { + m.documents[i].Note = msg.Note + } + } } - if m.state == stashStateStashLoaded { + if m.state == stashStateReady { // Update paginator m.paginator, cmd = paginator.Update(msg, m.paginator) @@ -243,7 +251,7 @@ func stashUpdate(msg boba.Msg, m stashModel) (stashModel, boba.Cmd) { // used for confirmation above) cancels the deletion k, ok := msg.(boba.KeyMsg) if ok && k.String() != "x" && m.state == stashStatePromptDelete { - m.state = stashStateStashLoaded + m.state = stashStateReady } return m, boba.Batch(cmds...) @@ -262,7 +270,7 @@ func stashView(m stashModel) string { s += spinner.View(m.spinner) + " Loading stash..." case stashStateLoadingDocument: s += spinner.View(m.spinner) + " Loading document..." - case stashStateStashLoaded: + case stashStateReady: fallthrough case stashStatePromptDelete: if len(m.documents) == 0 { diff --git a/ui/ui.go b/ui/ui.go index 1959f0d..08d1504 100644 --- a/ui/ui.go +++ b/ui/ui.go @@ -12,10 +12,6 @@ import ( te "github.com/muesli/termenv" ) -const ( - statusBarHeight = 1 -) - var ( glowLogoTextColor = common.Color("#ECFD65") statusBarBg = common.NewColorPair("#242424", "#E6E6E6") @@ -71,7 +67,7 @@ type model struct { func (m *model) unloadDocument() { m.state = stateShowStash - m.stash.state = stashStateStashLoaded + m.stash.state = stashStateReady m.pager.unload() } @@ -94,10 +90,8 @@ func initialize(style string) func() (boba.Model, boba.Cmd) { return model{ spinner: s, - pager: pagerModel{ - glamourStyle: style, - }, - state: stateInitCharmClient, + pager: newPagerModel(style), + state: stateInitCharmClient, }, boba.Batch( newCharmClient, spinner.Tick(s), @@ -124,20 +118,26 @@ func update(msg boba.Msg, mdl boba.Model) (boba.Model, boba.Cmd) { case boba.KeyMsg: switch msg.String() { - case "q": fallthrough case "esc": if m.state == stateShowDocument { - m.unloadDocument() - return m, nil + var cmd boba.Cmd + if m.pager.state == pagerStateBrowse { + // Exit pager + m.unloadDocument() + } else { + // Pass message through to pager + m.pager, cmd = pagerUpdate(msg, m.pager) + } + return m, cmd } return m, boba.Quit case "ctrl+c": return m, boba.Quit - // Re-render + // Repaint case "ctrl+l": return m, getTerminalSize() } @@ -192,14 +192,24 @@ func update(msg boba.Msg, mdl boba.Model) (boba.Model, boba.Cmd) { m.state = stateKeygenFinished cmds = append(cmds, newCharmClient) + case noteSavedMsg: + // A note was saved to a document. This will have be done in the + // pager, so we'll need to find the corresponding note in the stash. + // So, pass the message to the stash for processing. + m.stash, cmd = stashUpdate(msg, m.stash) + cmds = append(cmds, cmd) + case newCharmClientMsg: m.cc = msg m.state = stateShowStash - m.stash, cmd = stashInit(m.cc) + m.stash, cmd = stashInit(msg) m.stash.setSize(m.terminalWidth, m.terminalHeight) + m.pager.cc = msg cmds = append(cmds, cmd) case gotStashedItemMsg: + // Loaded markdown document from the server. We'll render it before + // loading it into the pager. m.pager.currentDocument = msg cmds = append(cmds, renderWithGlamour(m.pager, msg.Body))