2020-05-19 17:20:39 +00:00
|
|
|
package ui
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
2020-08-18 19:41:57 +00:00
|
|
|
"log"
|
2020-05-19 17:20:39 +00:00
|
|
|
"math"
|
|
|
|
"strings"
|
2020-08-21 00:21:52 +00:00
|
|
|
"time"
|
2020-05-19 17:20:39 +00:00
|
|
|
|
2020-08-21 22:49:45 +00:00
|
|
|
"github.com/charmbracelet/bubbles/spinner"
|
2020-05-26 17:42:25 +00:00
|
|
|
"github.com/charmbracelet/bubbles/textinput"
|
|
|
|
"github.com/charmbracelet/bubbles/viewport"
|
|
|
|
tea "github.com/charmbracelet/bubbletea"
|
2020-05-19 17:20:39 +00:00
|
|
|
"github.com/charmbracelet/charm"
|
2020-05-20 19:18:59 +00:00
|
|
|
"github.com/charmbracelet/charm/ui/common"
|
2020-05-19 17:20:39 +00:00
|
|
|
"github.com/charmbracelet/glamour"
|
2020-06-03 18:17:41 +00:00
|
|
|
runewidth "github.com/mattn/go-runewidth"
|
2020-07-15 16:56:57 +00:00
|
|
|
"github.com/muesli/reflow/ansi"
|
2020-05-19 17:20:39 +00:00
|
|
|
te "github.com/muesli/termenv"
|
|
|
|
)
|
|
|
|
|
2020-11-11 22:56:40 +00:00
|
|
|
const statusBarHeight = 1
|
2020-05-20 19:18:59 +00:00
|
|
|
|
|
|
|
var (
|
2020-10-19 03:11:47 +00:00
|
|
|
pagerHelpHeight int
|
2020-06-03 18:17:41 +00:00
|
|
|
|
2020-11-11 21:28:48 +00:00
|
|
|
mintGreen = common.NewColorPair("#89F0CB", "#89F0CB")
|
|
|
|
darkGreen = common.NewColorPair("#1C8760", "#1C8760")
|
|
|
|
|
2020-11-11 22:56:40 +00:00
|
|
|
noteHeading = te.String(" Set Memo ").
|
2020-05-20 20:45:09 +00:00
|
|
|
Foreground(common.Cream.Color()).
|
|
|
|
Background(common.Green.Color()).
|
|
|
|
String()
|
|
|
|
|
2020-08-24 16:55:38 +00:00
|
|
|
statusBarNoteFg = common.NewColorPair("#7D7D7D", "#656565")
|
2020-11-11 21:28:48 +00:00
|
|
|
statusBarBg = common.NewColorPair("#242424", "#E6E6E6")
|
2020-05-26 19:10:45 +00:00
|
|
|
|
2020-11-11 21:28:48 +00:00
|
|
|
// Styling funcs
|
2020-11-24 23:24:31 +00:00
|
|
|
statusBarScrollPosStyle = newStyle(common.NewColorPair("#5A5A5A", "#949494"), statusBarBg, false)
|
|
|
|
statusBarNoteStyle = newStyle(statusBarNoteFg, statusBarBg, false)
|
|
|
|
statusBarHelpStyle = newStyle(statusBarNoteFg, common.NewColorPair("#323232", "#DCDCDC"), false)
|
|
|
|
statusBarStashDotStyle = newStyle(common.Green, statusBarBg, false)
|
|
|
|
statusBarMessageStyle = newStyle(mintGreen, darkGreen, false)
|
|
|
|
statusBarMessageStashIconStyle = newStyle(mintGreen, darkGreen, false)
|
|
|
|
statusBarMessageScrollPosStyle = newStyle(mintGreen, darkGreen, false)
|
|
|
|
statusBarMessageHelpStyle = newStyle(common.NewColorPair("#B6FFE4", "#B6FFE4"), common.Green, false)
|
|
|
|
helpViewStyle = newStyle(statusBarNoteFg, common.NewColorPair("#1B1B1B", "#f2f2f2"), false)
|
2020-05-20 19:18:59 +00:00
|
|
|
)
|
|
|
|
|
2020-05-19 17:20:39 +00:00
|
|
|
type contentRenderedMsg string
|
2020-05-20 19:18:59 +00:00
|
|
|
type noteSavedMsg *charm.Markdown
|
2020-08-21 00:21:52 +00:00
|
|
|
type stashSuccessMsg markdown
|
2020-10-19 00:10:28 +00:00
|
|
|
type stashErrMsg struct{ err error }
|
2020-05-19 17:20:39 +00:00
|
|
|
|
2020-10-19 00:10:28 +00:00
|
|
|
func (s stashErrMsg) Error() string { return s.err.Error() }
|
2020-05-19 17:20:39 +00:00
|
|
|
|
|
|
|
type pagerState int
|
|
|
|
|
|
|
|
const (
|
2020-05-20 19:18:59 +00:00
|
|
|
pagerStateBrowse pagerState = iota
|
2020-05-19 17:20:39 +00:00
|
|
|
pagerStateSetNote
|
2020-08-21 00:21:52 +00:00
|
|
|
pagerStateStashing
|
2020-08-25 00:06:26 +00:00
|
|
|
pagerStateStashSuccess
|
2020-08-21 00:21:52 +00:00
|
|
|
pagerStateStatusMessage
|
2020-05-19 17:20:39 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
type pagerModel struct {
|
2020-11-17 22:37:52 +00:00
|
|
|
general *general
|
|
|
|
viewport viewport.Model
|
|
|
|
state pagerState
|
|
|
|
showHelp bool
|
|
|
|
textInput textinput.Model
|
|
|
|
spinner spinner.Model
|
2020-05-19 17:20:39 +00:00
|
|
|
|
2020-08-25 17:38:20 +00:00
|
|
|
statusMessage string
|
|
|
|
statusMessageTimer *time.Timer
|
2020-08-21 00:21:52 +00:00
|
|
|
|
2020-05-19 17:20:39 +00:00
|
|
|
// Current document being rendered, sans-glamour rendering. We cache
|
2020-08-13 19:37:53 +00:00
|
|
|
// it here so we can re-render it on resize.
|
2020-08-21 00:21:52 +00:00
|
|
|
currentDocument markdown
|
2020-08-25 00:06:26 +00:00
|
|
|
|
|
|
|
// Newly stashed markdown. We store it here temporarily so we can replace
|
|
|
|
// currentDocument above after a stash.
|
|
|
|
stashedDocument *markdown
|
2020-05-19 17:20:39 +00:00
|
|
|
}
|
|
|
|
|
2020-11-17 22:37:52 +00:00
|
|
|
func newPagerModel(general *general) pagerModel {
|
2020-06-20 23:54:01 +00:00
|
|
|
// Init viewport
|
|
|
|
vp := viewport.Model{}
|
|
|
|
vp.YPosition = 0
|
|
|
|
vp.HighPerformanceRendering = config.HighPerformancePager
|
|
|
|
|
2020-11-11 21:28:48 +00:00
|
|
|
// Text input for notes/memos
|
2020-05-20 19:18:59 +00:00
|
|
|
ti := textinput.NewModel()
|
2020-11-11 22:56:40 +00:00
|
|
|
ti.Prompt = te.String(" > ").
|
2020-11-11 21:28:48 +00:00
|
|
|
Foreground(common.Color(darkGray)).
|
|
|
|
Background(common.YellowGreen.Color()).
|
2020-05-20 19:18:59 +00:00
|
|
|
String()
|
2020-11-11 21:28:48 +00:00
|
|
|
ti.TextColor = darkGray
|
|
|
|
ti.BackgroundColor = common.YellowGreen.String()
|
|
|
|
ti.CursorColor = common.Fuschia.String()
|
2020-05-22 02:29:46 +00:00
|
|
|
ti.CharLimit = noteCharacterLimit
|
2020-05-20 19:18:59 +00:00
|
|
|
ti.Focus()
|
|
|
|
|
2020-11-11 21:28:48 +00:00
|
|
|
// Text input for search
|
2020-08-21 22:49:45 +00:00
|
|
|
sp := spinner.NewModel()
|
|
|
|
sp.ForegroundColor = statusBarNoteFg.String()
|
|
|
|
sp.BackgroundColor = statusBarBg.String()
|
2020-08-25 13:40:53 +00:00
|
|
|
sp.HideFor = time.Millisecond * 50
|
2020-08-25 00:06:26 +00:00
|
|
|
sp.MinimumLifetime = time.Millisecond * 180
|
2020-08-21 22:49:45 +00:00
|
|
|
|
2020-05-20 19:18:59 +00:00
|
|
|
return pagerModel{
|
2020-11-17 22:37:52 +00:00
|
|
|
general: general,
|
|
|
|
state: pagerStateBrowse,
|
|
|
|
textInput: ti,
|
|
|
|
viewport: vp,
|
|
|
|
spinner: sp,
|
2020-05-20 19:18:59 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-19 17:20:39 +00:00
|
|
|
func (m *pagerModel) setSize(w, h int) {
|
|
|
|
m.viewport.Width = w
|
2020-05-20 19:18:59 +00:00
|
|
|
m.viewport.Height = h - statusBarHeight
|
2020-11-11 22:56:40 +00:00
|
|
|
m.textInput.Width = w -
|
|
|
|
ansi.PrintableRuneWidth(noteHeading) -
|
|
|
|
ansi.PrintableRuneWidth(m.textInput.Prompt) - 1
|
2020-06-03 18:17:41 +00:00
|
|
|
|
|
|
|
if m.showHelp {
|
2020-10-19 00:10:28 +00:00
|
|
|
if pagerHelpHeight == 0 {
|
|
|
|
pagerHelpHeight = strings.Count(m.helpView(), "\n")
|
|
|
|
}
|
2020-06-20 19:21:30 +00:00
|
|
|
m.viewport.Height -= (statusBarHeight + pagerHelpHeight)
|
2020-06-03 18:17:41 +00:00
|
|
|
}
|
2020-05-19 17:20:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (m *pagerModel) setContent(s string) {
|
|
|
|
m.viewport.SetContent(s)
|
|
|
|
}
|
|
|
|
|
2020-07-17 22:55:02 +00:00
|
|
|
func (m *pagerModel) toggleHelp() {
|
2020-06-08 17:15:02 +00:00
|
|
|
m.showHelp = !m.showHelp
|
2020-11-17 22:37:52 +00:00
|
|
|
m.setSize(m.general.width, m.general.height)
|
2020-07-17 23:23:34 +00:00
|
|
|
if m.viewport.PastBottom() {
|
|
|
|
m.viewport.GotoBottom()
|
|
|
|
}
|
2020-06-08 17:15:02 +00:00
|
|
|
}
|
|
|
|
|
2020-08-25 00:06:26 +00:00
|
|
|
// Perform stuff that needs to happen after a successful markdown stash. Note
|
|
|
|
// that the the returned command should be sent back the through the pager
|
|
|
|
// update function.
|
|
|
|
func (m *pagerModel) showStatusMessage(statusMessage string) tea.Cmd {
|
|
|
|
// Show a success message to the user
|
|
|
|
m.state = pagerStateStatusMessage
|
2020-08-25 17:38:20 +00:00
|
|
|
m.statusMessage = statusMessage
|
2020-08-25 00:06:26 +00:00
|
|
|
if m.statusMessageTimer != nil {
|
|
|
|
m.statusMessageTimer.Stop()
|
|
|
|
}
|
|
|
|
m.statusMessageTimer = time.NewTimer(statusMessageTimeout)
|
|
|
|
|
|
|
|
return waitForStatusMessageTimeout(pagerContext, m.statusMessageTimer)
|
|
|
|
}
|
|
|
|
|
2020-05-19 17:20:39 +00:00
|
|
|
func (m *pagerModel) unload() {
|
2020-06-08 17:15:02 +00:00
|
|
|
if m.showHelp {
|
2020-07-17 22:55:02 +00:00
|
|
|
m.toggleHelp()
|
2020-06-08 17:15:02 +00:00
|
|
|
}
|
2020-08-21 00:21:52 +00:00
|
|
|
if m.statusMessageTimer != nil {
|
|
|
|
m.statusMessageTimer.Stop()
|
|
|
|
}
|
2020-05-20 19:18:59 +00:00
|
|
|
m.state = pagerStateBrowse
|
2020-05-19 17:20:39 +00:00
|
|
|
m.viewport.SetContent("")
|
2020-06-20 18:49:56 +00:00
|
|
|
m.viewport.YOffset = 0
|
2020-05-20 19:18:59 +00:00
|
|
|
m.textInput.Reset()
|
2020-05-19 17:20:39 +00:00
|
|
|
}
|
|
|
|
|
2020-11-25 16:40:24 +00:00
|
|
|
func (m pagerModel) update(msg tea.Msg) (pagerModel, tea.Cmd) {
|
2020-05-20 19:18:59 +00:00
|
|
|
var (
|
2020-05-26 17:42:25 +00:00
|
|
|
cmd tea.Cmd
|
|
|
|
cmds []tea.Cmd
|
2020-05-20 19:18:59 +00:00
|
|
|
)
|
|
|
|
|
2020-05-19 17:20:39 +00:00
|
|
|
switch msg := msg.(type) {
|
2020-05-26 17:42:25 +00:00
|
|
|
case tea.KeyMsg:
|
2020-05-20 19:18:59 +00:00
|
|
|
switch m.state {
|
|
|
|
case pagerStateSetNote:
|
|
|
|
switch msg.String() {
|
|
|
|
case "esc":
|
|
|
|
m.state = pagerStateBrowse
|
|
|
|
return m, nil
|
|
|
|
case "enter":
|
2020-05-26 17:42:25 +00:00
|
|
|
var cmd tea.Cmd
|
2020-05-20 20:27:19 +00:00
|
|
|
if m.textInput.Value() != m.currentDocument.Note { // don't update if the note didn't change
|
|
|
|
m.currentDocument.Note = m.textInput.Value() // update optimistically
|
2020-11-17 22:37:52 +00:00
|
|
|
cmd = saveDocumentNote(m.general.cc, m.currentDocument.ID, m.currentDocument.Note)
|
2020-05-20 20:27:19 +00:00
|
|
|
}
|
2020-05-20 19:18:59 +00:00
|
|
|
m.state = pagerStateBrowse
|
|
|
|
m.textInput.Reset()
|
2020-05-20 20:27:19 +00:00
|
|
|
return m, cmd
|
2020-05-20 19:18:59 +00:00
|
|
|
}
|
|
|
|
default:
|
|
|
|
switch msg.String() {
|
2020-07-20 23:22:12 +00:00
|
|
|
case "q", "esc":
|
2020-05-20 19:18:59 +00:00
|
|
|
if m.state != pagerStateBrowse {
|
|
|
|
m.state = pagerStateBrowse
|
|
|
|
return m, nil
|
|
|
|
}
|
2020-08-25 23:01:22 +00:00
|
|
|
case "home", "g":
|
|
|
|
m.viewport.GotoTop()
|
|
|
|
if m.viewport.HighPerformanceRendering {
|
|
|
|
cmds = append(cmds, viewport.Sync(m.viewport))
|
|
|
|
}
|
|
|
|
case "end", "G":
|
|
|
|
m.viewport.GotoBottom()
|
|
|
|
if m.viewport.HighPerformanceRendering {
|
|
|
|
cmds = append(cmds, viewport.Sync(m.viewport))
|
|
|
|
}
|
2020-06-03 18:24:40 +00:00
|
|
|
case "m":
|
2020-11-25 16:40:24 +00:00
|
|
|
isStashed := m.currentDocument.markdownType == StashedDocument ||
|
|
|
|
m.currentDocument.markdownType == ConvertedDocument
|
2020-08-24 19:14:47 +00:00
|
|
|
|
2020-07-14 20:16:26 +00:00
|
|
|
// Users can only set the note on user-stashed markdown
|
2020-08-24 19:14:47 +00:00
|
|
|
if !isStashed {
|
2020-05-21 19:14:33 +00:00
|
|
|
break
|
|
|
|
}
|
|
|
|
|
2020-05-20 19:18:59 +00:00
|
|
|
m.state = pagerStateSetNote
|
2020-08-21 00:21:52 +00:00
|
|
|
|
|
|
|
// 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
|
2020-05-20 20:17:18 +00:00
|
|
|
if m.textInput.Value() == "" {
|
|
|
|
m.textInput.SetValue(m.currentDocument.Note)
|
|
|
|
m.textInput.CursorEnd()
|
|
|
|
}
|
2020-08-21 00:21:52 +00:00
|
|
|
|
2020-11-13 21:17:50 +00:00
|
|
|
return m, textinput.Blink
|
2020-08-21 00:21:52 +00:00
|
|
|
case "s":
|
2020-11-17 22:37:52 +00:00
|
|
|
if m.general.authStatus != authOK {
|
2020-09-10 17:32:57 +00:00
|
|
|
break
|
|
|
|
}
|
|
|
|
|
2020-08-21 00:21:52 +00:00
|
|
|
// Stash a local document
|
2020-11-25 16:40:24 +00:00
|
|
|
if m.state != pagerStateStashing && m.currentDocument.markdownType == LocalDocument {
|
2020-08-21 00:21:52 +00:00
|
|
|
m.state = pagerStateStashing
|
2020-08-25 00:06:26 +00:00
|
|
|
m.spinner.Start()
|
2020-08-21 22:49:45 +00:00
|
|
|
cmds = append(
|
|
|
|
cmds,
|
2020-11-17 22:37:52 +00:00
|
|
|
stashDocument(m.general.cc, m.currentDocument),
|
2020-11-13 21:17:50 +00:00
|
|
|
spinner.Tick,
|
2020-08-21 22:49:45 +00:00
|
|
|
)
|
2020-08-21 00:21:52 +00:00
|
|
|
}
|
2020-06-03 18:17:41 +00:00
|
|
|
case "?":
|
2020-07-17 22:55:02 +00:00
|
|
|
m.toggleHelp()
|
2020-06-19 23:05:45 +00:00
|
|
|
if m.viewport.HighPerformanceRendering {
|
|
|
|
cmds = append(cmds, viewport.Sync(m.viewport))
|
|
|
|
}
|
2020-05-20 19:18:59 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-21 22:49:45 +00:00
|
|
|
case spinner.TickMsg:
|
2020-08-25 13:40:53 +00:00
|
|
|
if m.state == pagerStateStashing || m.spinner.Visible() {
|
2020-11-13 21:17:50 +00:00
|
|
|
newSpinnerModel, cmd := m.spinner.Update(msg)
|
2020-08-21 22:49:45 +00:00
|
|
|
m.spinner = newSpinnerModel
|
|
|
|
cmds = append(cmds, cmd)
|
2020-08-25 13:40:53 +00:00
|
|
|
} else if m.state == pagerStateStashSuccess && !m.spinner.Visible() {
|
2020-08-25 00:06:26 +00:00
|
|
|
m.state = pagerStateBrowse
|
|
|
|
m.currentDocument = *m.stashedDocument
|
|
|
|
m.stashedDocument = nil
|
|
|
|
cmd := m.showStatusMessage("Stashed!")
|
|
|
|
cmds = append(cmds, cmd)
|
2020-08-21 22:49:45 +00:00
|
|
|
}
|
|
|
|
|
2020-05-19 17:20:39 +00:00
|
|
|
// Glow has rendered the content
|
|
|
|
case contentRenderedMsg:
|
|
|
|
m.setContent(string(msg))
|
2020-06-19 23:05:45 +00:00
|
|
|
if m.viewport.HighPerformanceRendering {
|
|
|
|
cmds = append(cmds, viewport.Sync(m.viewport))
|
|
|
|
}
|
2020-05-19 17:20:39 +00:00
|
|
|
|
|
|
|
// We've reveived terminal dimensions, either for the first time or
|
|
|
|
// after a resize
|
2020-06-19 18:56:30 +00:00
|
|
|
case tea.WindowSizeMsg:
|
2020-08-21 00:21:52 +00:00
|
|
|
return m, renderWithGlamour(m, m.currentDocument.Body)
|
|
|
|
|
|
|
|
case stashSuccessMsg:
|
|
|
|
// Stashing was successful. Convert the loaded document to a stashed
|
2020-08-25 00:06:26 +00:00
|
|
|
// one and show a status message. 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 = pagerStateStashSuccess
|
2020-08-25 13:40:53 +00:00
|
|
|
if !m.spinner.Visible() {
|
2020-08-25 00:06:26 +00:00
|
|
|
m.state = pagerStateBrowse
|
|
|
|
m.currentDocument = markdown(msg)
|
|
|
|
cmd := m.showStatusMessage("Stashed!")
|
|
|
|
cmds = append(cmds, cmd)
|
|
|
|
} else {
|
|
|
|
md := markdown(msg)
|
|
|
|
m.stashedDocument = &md
|
2020-08-21 17:39:59 +00:00
|
|
|
}
|
2020-08-21 00:21:52 +00:00
|
|
|
|
|
|
|
case stashErrMsg:
|
|
|
|
// TODO
|
|
|
|
|
|
|
|
case statusMessageTimeoutMsg:
|
|
|
|
// Hide the status message bar
|
|
|
|
m.state = pagerStateBrowse
|
2020-05-19 17:20:39 +00:00
|
|
|
}
|
|
|
|
|
2020-05-20 19:18:59 +00:00
|
|
|
switch m.state {
|
|
|
|
case pagerStateSetNote:
|
2020-11-13 21:17:50 +00:00
|
|
|
m.textInput, cmd = m.textInput.Update(msg)
|
2020-05-20 19:18:59 +00:00
|
|
|
cmds = append(cmds, cmd)
|
|
|
|
default:
|
2020-11-13 21:17:50 +00:00
|
|
|
m.viewport, cmd = m.viewport.Update(msg)
|
2020-05-20 19:18:59 +00:00
|
|
|
cmds = append(cmds, cmd)
|
|
|
|
}
|
2020-05-19 17:20:39 +00:00
|
|
|
|
2020-05-26 17:42:25 +00:00
|
|
|
return m, tea.Batch(cmds...)
|
2020-05-19 17:20:39 +00:00
|
|
|
}
|
|
|
|
|
2020-10-19 00:10:28 +00:00
|
|
|
func (m pagerModel) View() string {
|
2020-05-26 19:10:45 +00:00
|
|
|
var b strings.Builder
|
2020-11-13 21:17:50 +00:00
|
|
|
fmt.Fprint(&b, m.viewport.View()+"\n")
|
2020-05-26 19:10:45 +00:00
|
|
|
|
|
|
|
// Footer
|
2020-08-21 00:21:52 +00:00
|
|
|
switch m.state {
|
|
|
|
case pagerStateSetNote:
|
2020-10-19 00:10:28 +00:00
|
|
|
m.setNoteView(&b)
|
2020-08-21 00:21:52 +00:00
|
|
|
default:
|
2020-10-19 00:10:28 +00:00
|
|
|
m.statusBarView(&b)
|
2020-05-20 19:18:59 +00:00
|
|
|
}
|
|
|
|
|
2020-06-03 18:17:41 +00:00
|
|
|
if m.showHelp {
|
2020-10-19 00:10:28 +00:00
|
|
|
fmt.Fprint(&b, m.helpView())
|
2020-06-03 18:17:41 +00:00
|
|
|
}
|
|
|
|
|
2020-05-26 19:10:45 +00:00
|
|
|
return b.String()
|
2020-05-19 17:20:39 +00:00
|
|
|
}
|
|
|
|
|
2020-10-19 00:10:28 +00:00
|
|
|
func (m pagerModel) statusBarView(b *strings.Builder) {
|
2020-08-25 17:36:42 +00:00
|
|
|
const (
|
|
|
|
minPercent float64 = 0.0
|
|
|
|
maxPercent float64 = 1.0
|
|
|
|
percentToStringMagnitude float64 = 100.0
|
|
|
|
)
|
2020-08-24 16:55:38 +00:00
|
|
|
var (
|
2020-11-25 16:40:24 +00:00
|
|
|
isStashed bool = m.currentDocument.markdownType == StashedDocument || m.currentDocument.markdownType == ConvertedDocument
|
2020-08-24 16:55:38 +00:00
|
|
|
showStatusMessage bool = m.state == pagerStateStatusMessage
|
|
|
|
)
|
|
|
|
|
2020-05-19 17:20:39 +00:00
|
|
|
// Logo
|
2020-07-15 16:56:57 +00:00
|
|
|
logo := glowLogoView(" Glow ")
|
2020-05-19 17:20:39 +00:00
|
|
|
|
|
|
|
// Scroll percent
|
2020-08-25 17:36:42 +00:00
|
|
|
percent := math.Max(minPercent, math.Min(maxPercent, m.viewport.ScrollPercent()))
|
|
|
|
scrollPercent := fmt.Sprintf(" %3.f%% ", percent*percentToStringMagnitude)
|
2020-08-24 16:55:38 +00:00
|
|
|
if showStatusMessage {
|
|
|
|
scrollPercent = statusBarMessageScrollPosStyle(scrollPercent)
|
|
|
|
} else {
|
|
|
|
scrollPercent = statusBarScrollPosStyle(scrollPercent)
|
|
|
|
}
|
2020-05-19 17:20:39 +00:00
|
|
|
|
2020-06-03 18:17:41 +00:00
|
|
|
// "Help" note
|
2020-08-24 16:55:38 +00:00
|
|
|
var helpNote string
|
|
|
|
if showStatusMessage {
|
|
|
|
helpNote = statusBarMessageHelpStyle(" ? Help ")
|
|
|
|
} else {
|
|
|
|
helpNote = statusBarHelpStyle(" ? Help ")
|
|
|
|
}
|
2020-06-03 18:17:41 +00:00
|
|
|
|
2020-08-21 22:06:17 +00:00
|
|
|
// Status indicator; spinner or stash dot
|
|
|
|
var statusIndicator string
|
2020-08-25 00:06:26 +00:00
|
|
|
if m.state == pagerStateStashing || m.state == pagerStateStashSuccess {
|
2020-08-25 13:40:53 +00:00
|
|
|
if m.spinner.Visible() {
|
2020-11-13 21:17:50 +00:00
|
|
|
statusIndicator = statusBarNoteStyle(" ") + m.spinner.View()
|
2020-08-25 00:06:26 +00:00
|
|
|
}
|
2020-08-24 16:55:38 +00:00
|
|
|
} else if isStashed && showStatusMessage {
|
2020-10-13 15:27:41 +00:00
|
|
|
statusIndicator = statusBarMessageStashIconStyle(" " + pagerStashIcon)
|
2020-08-24 16:55:38 +00:00
|
|
|
} else if isStashed {
|
2020-08-25 13:56:17 +00:00
|
|
|
statusIndicator = statusBarStashDotStyle(" " + pagerStashIcon)
|
2020-08-21 22:06:17 +00:00
|
|
|
}
|
|
|
|
|
2020-05-19 17:20:39 +00:00
|
|
|
// Note
|
2020-08-24 16:55:38 +00:00
|
|
|
var note string
|
|
|
|
if showStatusMessage {
|
2020-12-07 13:22:38 +00:00
|
|
|
note = m.statusMessage
|
2020-08-24 16:55:38 +00:00
|
|
|
} else {
|
|
|
|
note = m.currentDocument.Note
|
|
|
|
if len(note) == 0 {
|
|
|
|
note = "(No memo)"
|
|
|
|
}
|
2020-05-19 17:20:39 +00:00
|
|
|
}
|
2020-08-24 16:55:38 +00:00
|
|
|
note = truncate(" "+note+" ", max(0,
|
2020-11-17 22:37:52 +00:00
|
|
|
m.general.width-
|
2020-07-15 16:56:57 +00:00
|
|
|
ansi.PrintableRuneWidth(logo)-
|
2020-08-21 22:06:17 +00:00
|
|
|
ansi.PrintableRuneWidth(statusIndicator)-
|
2020-08-24 16:55:38 +00:00
|
|
|
ansi.PrintableRuneWidth(scrollPercent)-
|
2020-07-15 16:56:57 +00:00
|
|
|
ansi.PrintableRuneWidth(helpNote),
|
2020-06-03 18:17:41 +00:00
|
|
|
))
|
2020-08-24 16:55:38 +00:00
|
|
|
if showStatusMessage {
|
2020-08-25 17:38:20 +00:00
|
|
|
note = statusBarMessageStyle(note)
|
2020-08-24 16:55:38 +00:00
|
|
|
} else {
|
|
|
|
note = statusBarNoteStyle(note)
|
|
|
|
}
|
2020-05-19 17:20:39 +00:00
|
|
|
|
|
|
|
// Empty space
|
2020-07-15 16:56:57 +00:00
|
|
|
padding := max(0,
|
2020-11-17 22:37:52 +00:00
|
|
|
m.general.width-
|
2020-07-15 16:56:57 +00:00
|
|
|
ansi.PrintableRuneWidth(logo)-
|
2020-08-21 22:06:17 +00:00
|
|
|
ansi.PrintableRuneWidth(statusIndicator)-
|
2020-08-24 16:55:38 +00:00
|
|
|
ansi.PrintableRuneWidth(note)-
|
|
|
|
ansi.PrintableRuneWidth(scrollPercent)-
|
2020-07-15 16:56:57 +00:00
|
|
|
ansi.PrintableRuneWidth(helpNote),
|
|
|
|
)
|
2020-08-24 16:55:38 +00:00
|
|
|
emptySpace := strings.Repeat(" ", padding)
|
|
|
|
if showStatusMessage {
|
2020-08-25 17:38:20 +00:00
|
|
|
emptySpace = statusBarMessageStyle(emptySpace)
|
2020-08-24 16:55:38 +00:00
|
|
|
} else {
|
|
|
|
emptySpace = statusBarNoteStyle(emptySpace)
|
|
|
|
}
|
2020-05-19 17:20:39 +00:00
|
|
|
|
2020-08-21 22:06:17 +00:00
|
|
|
fmt.Fprintf(b, "%s%s%s%s%s%s",
|
2020-05-26 19:10:45 +00:00
|
|
|
logo,
|
2020-08-21 22:06:17 +00:00
|
|
|
statusIndicator,
|
2020-08-24 16:55:38 +00:00
|
|
|
note,
|
2020-05-26 19:10:45 +00:00
|
|
|
emptySpace,
|
2020-08-24 16:55:38 +00:00
|
|
|
scrollPercent,
|
2020-06-03 18:17:41 +00:00
|
|
|
helpNote,
|
2020-05-26 19:10:45 +00:00
|
|
|
)
|
2020-05-19 17:20:39 +00:00
|
|
|
}
|
|
|
|
|
2020-10-19 00:10:28 +00:00
|
|
|
func (m pagerModel) setNoteView(b *strings.Builder) {
|
2020-05-26 19:10:45 +00:00
|
|
|
fmt.Fprint(b, noteHeading)
|
2020-11-13 21:17:50 +00:00
|
|
|
fmt.Fprint(b, m.textInput.View())
|
2020-05-20 19:18:59 +00:00
|
|
|
}
|
|
|
|
|
2020-10-19 00:10:28 +00:00
|
|
|
func (m pagerModel) helpView() (s string) {
|
2020-10-19 02:12:08 +00:00
|
|
|
memoOrStash := "m set memo"
|
2020-11-25 16:40:24 +00:00
|
|
|
if m.general.authStatus == authOK && m.currentDocument.markdownType == LocalDocument {
|
2020-10-19 02:12:08 +00:00
|
|
|
memoOrStash = "s stash this document"
|
|
|
|
}
|
|
|
|
|
2020-10-19 15:26:52 +00:00
|
|
|
col1 := []string{
|
2020-08-25 23:01:22 +00:00
|
|
|
"g/home go to top",
|
|
|
|
"G/end go to bottom",
|
|
|
|
"",
|
2020-10-19 02:12:08 +00:00
|
|
|
memoOrStash,
|
2020-07-21 01:29:03 +00:00
|
|
|
"esc back to files",
|
|
|
|
"q quit",
|
2020-07-14 22:05:36 +00:00
|
|
|
}
|
|
|
|
|
2020-11-25 16:40:24 +00:00
|
|
|
if m.currentDocument.markdownType == NewsDocument {
|
2020-10-19 15:26:52 +00:00
|
|
|
deleteFromStringSlice(col1, 3)
|
|
|
|
}
|
|
|
|
|
2020-06-03 18:17:41 +00:00
|
|
|
s += "\n"
|
2020-07-14 22:05:36 +00:00
|
|
|
s += "k/↑ up " + col1[0] + "\n"
|
2020-10-09 18:27:19 +00:00
|
|
|
s += "j/↓ down " + col1[1] + "\n"
|
2020-10-09 04:22:54 +00:00
|
|
|
s += "b/pgup page up " + col1[2] + "\n"
|
|
|
|
s += "f/pgdn page down " + col1[3] + "\n"
|
|
|
|
s += "u ½ page up " + col1[4] + "\n"
|
2020-10-19 15:26:52 +00:00
|
|
|
s += "d ½ page down "
|
|
|
|
|
|
|
|
if len(col1) > 5 {
|
|
|
|
s += col1[5]
|
|
|
|
}
|
2020-06-03 18:17:41 +00:00
|
|
|
|
|
|
|
s = indent(s, 2)
|
|
|
|
|
|
|
|
// Fill up empty cells with spaces for background coloring
|
2020-11-17 22:37:52 +00:00
|
|
|
if m.general.width > 0 {
|
2020-06-03 18:17:41 +00:00
|
|
|
lines := strings.Split(s, "\n")
|
|
|
|
for i := 0; i < len(lines); i++ {
|
|
|
|
l := runewidth.StringWidth(lines[i])
|
2020-11-17 22:37:52 +00:00
|
|
|
n := max(m.general.width-l, 0)
|
2020-06-03 18:17:41 +00:00
|
|
|
lines[i] += strings.Repeat(" ", n)
|
|
|
|
}
|
|
|
|
|
|
|
|
s = strings.Join(lines, "\n")
|
|
|
|
}
|
|
|
|
|
|
|
|
return helpViewStyle(s)
|
|
|
|
}
|
|
|
|
|
2020-08-24 20:48:10 +00:00
|
|
|
// COMMANDS
|
2020-05-19 17:20:39 +00:00
|
|
|
|
2020-05-26 17:42:25 +00:00
|
|
|
func renderWithGlamour(m pagerModel, md string) tea.Cmd {
|
|
|
|
return func() tea.Msg {
|
2020-05-19 17:20:39 +00:00
|
|
|
s, err := glamourRender(m, md)
|
|
|
|
if err != nil {
|
2020-08-18 19:41:57 +00:00
|
|
|
if debug {
|
|
|
|
log.Println("error rendering with Glamour:", err)
|
|
|
|
}
|
2020-08-21 20:13:38 +00:00
|
|
|
return errMsg{err}
|
2020-05-19 17:20:39 +00:00
|
|
|
}
|
|
|
|
return contentRenderedMsg(s)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-24 20:07:43 +00:00
|
|
|
// This is where the magic happens.
|
2020-05-19 17:20:39 +00:00
|
|
|
func glamourRender(m pagerModel, markdown string) (string, error) {
|
2020-06-20 23:54:01 +00:00
|
|
|
if !config.GlamourEnabled {
|
2020-05-19 17:20:39 +00:00
|
|
|
return markdown, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// initialize glamour
|
|
|
|
var gs glamour.TermRendererOption
|
2020-11-17 22:37:52 +00:00
|
|
|
if m.general.cfg.GlamourStyle == "auto" {
|
2020-05-19 17:20:39 +00:00
|
|
|
gs = glamour.WithAutoStyle()
|
|
|
|
} else {
|
2020-11-17 22:37:52 +00:00
|
|
|
gs = glamour.WithStylePath(m.general.cfg.GlamourStyle)
|
2020-05-19 17:20:39 +00:00
|
|
|
}
|
|
|
|
|
2020-11-17 22:37:52 +00:00
|
|
|
width := max(0, min(int(m.general.cfg.GlamourMaxWidth), m.viewport.Width))
|
2020-05-19 17:20:39 +00:00
|
|
|
r, err := glamour.NewTermRenderer(
|
|
|
|
gs,
|
2020-05-20 20:54:37 +00:00
|
|
|
glamour.WithWordWrap(width),
|
2020-05-19 17:20:39 +00:00
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
out, err := r.Render(markdown)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
// trim lines
|
2020-08-24 20:08:28 +00:00
|
|
|
lines := strings.Split(out, "\n")
|
2020-05-19 17:20:39 +00:00
|
|
|
|
|
|
|
var content string
|
|
|
|
for i, s := range lines {
|
|
|
|
content += strings.TrimSpace(s)
|
|
|
|
|
|
|
|
// don't add an artificial newline after the last split
|
|
|
|
if i+1 < len(lines) {
|
|
|
|
content += "\n"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return content, nil
|
|
|
|
}
|
2020-10-19 15:26:52 +00:00
|
|
|
|
|
|
|
// ETC
|
|
|
|
|
|
|
|
// Note: this runs in linear time; O(n).
|
|
|
|
func deleteFromStringSlice(a []string, i int) []string {
|
|
|
|
copy(a[i:], a[i+1:])
|
|
|
|
a[len(a)-1] = ""
|
|
|
|
return a[:len(a)-1]
|
|
|
|
}
|