glow/ui/stash.go

709 lines
18 KiB
Go
Raw Normal View History

package ui
import (
2020-05-13 23:02:39 +00:00
"fmt"
2020-05-19 00:45:13 +00:00
"math"
2020-05-13 23:02:39 +00:00
"sort"
"strconv"
2020-05-13 23:02:39 +00:00
"strings"
2020-05-19 00:45:13 +00:00
"time"
2020-05-13 23:02:39 +00:00
"github.com/charmbracelet/boba"
2020-05-15 00:23:11 +00:00
"github.com/charmbracelet/boba/paginator"
"github.com/charmbracelet/boba/spinner"
2020-05-22 02:29:46 +00:00
"github.com/charmbracelet/boba/textinput"
"github.com/charmbracelet/charm"
"github.com/charmbracelet/charm/ui/common"
2020-05-19 00:45:13 +00:00
"github.com/dustin/go-humanize"
2020-05-13 23:02:39 +00:00
"github.com/muesli/reflow/indent"
te "github.com/muesli/termenv"
)
2020-05-15 00:23:11 +00:00
const (
2020-05-22 02:29:46 +00:00
stashViewItemHeight = 3
stashViewTopPadding = 5
stashViewBottomPadding = 4
stashViewHorizontalPadding = 6
setNotePromptText = "Memo: "
2020-05-15 00:23:11 +00:00
)
2020-05-21 19:14:33 +00:00
var (
2020-05-22 02:29:46 +00:00
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")
2020-05-21 19:14:33 +00:00
)
// MSG
type stashSpinnerTickMsg struct{}
2020-05-14 02:08:17 +00:00
type gotStashMsg []*charm.Markdown
2020-05-21 19:14:33 +00:00
type gotNewsMsg []*charm.Markdown
2020-05-22 19:31:54 +00:00
type fetchedMarkdownMsg *markdown
2020-05-15 22:34:42 +00:00
type deletedStashedItemMsg int
// MODEL
2020-05-22 02:29:46 +00:00
// markdownType allows us to differentiate between the types of markdown
// documents we're dealing with, namely stuff the user stashed versus news.
2020-05-21 19:14:33 +00:00
type markdownType int
const (
userMarkdown markdownType = iota
newsMarkdown
)
// markdown wraps charm.Markdown so we can differentiate between stashed items
2020-05-22 02:29:46 +00:00
// and news.
2020-05-21 19:14:33 +00:00
type markdown struct {
markdownType markdownType
*charm.Markdown
}
// Sort documents by date in descending order
type markdownsByCreatedAtDesc []*markdown
// Sort implementation for MarkdownByCreatedAt
func (m markdownsByCreatedAtDesc) Len() int { return len(m) }
func (m markdownsByCreatedAtDesc) Swap(i, j int) { m[i], m[j] = m[j], m[i] }
func (m markdownsByCreatedAtDesc) Less(i, j int) bool { return m[i].CreatedAt.After(*m[j].CreatedAt) }
type stashState int
const (
stashStateInit stashState = iota
stashStateReady
2020-05-15 22:34:42 +00:00
stashStatePromptDelete
2020-05-15 19:08:45 +00:00
stashStateLoadingDocument
2020-05-22 02:29:46 +00:00
stashStateSettingNote
)
type stashLoadedState byte
func (s stashLoadedState) done() bool {
return s&loadedStash != 0 && s&loadedNews != 0
}
const (
loadedStash stashLoadedState = 1 << iota
loadedNews
)
type stashModel struct {
2020-05-15 00:23:11 +00:00
cc *charm.Client
state stashState
2020-05-21 19:14:33 +00:00
documents []*markdown
2020-05-15 00:23:11 +00:00
spinner spinner.Model
2020-05-22 02:29:46 +00:00
noteInput textinput.Model
2020-05-15 00:23:11 +00:00
terminalWidth int
terminalHeight int
loaded stashLoadedState // what's loaded? we find out with bitmasking
loading bool // are we currently loading something?
fullyLoaded bool // Have we loaded everything from the server?
2020-05-15 00:23:11 +00:00
2020-05-21 19:14:33 +00:00
// This is just the index of the current page in view. To get the index
// of the selected item as it relates to the full set of documents we've
// fetched use the mardownIndex() method of this struct.
index int
2020-05-15 00:23:11 +00:00
// This handles the local pagination, which is different than the page
// we're fetching from on the server side
paginator paginator.Model
2020-05-21 19:14:33 +00:00
// Page we're fetching stash items from on the server, which is different
// from the local pagination. Generally, the server will return more items
// than we can display at a time so we can paginate locally without having
// to fetch every time.
2020-05-15 00:23:11 +00:00
page int
}
func (m *stashModel) setSize(width, height int) {
2020-05-15 00:23:11 +00:00
m.terminalWidth = width
m.terminalHeight = height
// Update the paginator
perPage := max(1, (m.terminalHeight-stashViewTopPadding-stashViewBottomPadding)/stashViewItemHeight)
2020-05-15 00:23:11 +00:00
m.paginator.PerPage = perPage
m.paginator.SetTotalPages(len(m.documents))
2020-05-22 02:29:46 +00:00
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)
}
}
2020-05-21 19:14:33 +00:00
// markdownIndex returns the index of the currently selected markdown item.
func (m stashModel) markdownIndex() int {
return m.paginator.Page*m.paginator.PerPage + m.index
}
2020-05-22 20:01:23 +00:00
// addMarkdowns adds markdown documents to the model
func (m *stashModel) addMarkdowns(mds ...*markdown) {
m.documents = append(m.documents, mds...)
sort.Sort(markdownsByCreatedAtDesc(m.documents))
m.paginator.SetTotalPages(len(m.documents))
}
// INIT
func stashInit(cc *charm.Client) (stashModel, boba.Cmd) {
s := spinner.NewModel()
s.Type = spinner.Dot
s.ForegroundColor = common.SpinnerColor
2020-05-15 00:23:11 +00:00
s.CustomMsgFunc = func() boba.Msg { return stashSpinnerTickMsg{} }
2020-05-15 00:52:25 +00:00
p := paginator.NewModel()
p.Type = paginator.Dots
p.InactiveDot = common.Subtle("•")
2020-05-22 02:29:46 +00:00
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{
2020-05-15 00:23:11 +00:00
cc: cc,
spinner: s,
2020-05-22 02:29:46 +00:00
noteInput: ni,
2020-05-15 00:23:11 +00:00
page: 1,
2020-05-15 00:52:25 +00:00
paginator: p,
}
return m, boba.Batch(
2020-05-14 02:08:17 +00:00
loadStash(m),
2020-05-21 19:14:33 +00:00
loadNews(m),
spinner.Tick(s),
)
}
// UPDATE
func stashUpdate(msg boba.Msg, m stashModel) (stashModel, boba.Cmd) {
2020-05-15 00:52:25 +00:00
var (
cmd boba.Cmd
cmds []boba.Cmd
)
switch msg := msg.(type) {
// Stash results have come in from the server
case gotStashMsg:
m.loading = false
// If the server comes back with nothing then we've got everything
if len(msg) == 0 {
m.fullyLoaded = true
break
}
2020-05-21 19:14:33 +00:00
docs := wrapMarkdowns(userMarkdown, msg)
2020-05-22 20:01:23 +00:00
m.addMarkdowns(docs...)
m.loaded |= loadedStash
if m.loaded.done() {
m.state = stashStateReady
}
2020-05-21 19:14:33 +00:00
case gotNewsMsg:
if len(msg) > 0 {
docs := wrapMarkdowns(newsMarkdown, msg)
2020-05-22 20:01:23 +00:00
m.addMarkdowns(docs...)
2020-05-21 19:14:33 +00:00
}
m.loaded |= loadedNews
if m.loaded.done() {
m.state = stashStateReady
}
2020-05-21 19:14:33 +00:00
case stashSpinnerTickMsg:
2020-05-15 19:08:45 +00:00
if m.state == stashStateInit || m.state == stashStateLoadingDocument {
m.spinner, cmd = spinner.Update(msg, m.spinner)
2020-05-15 22:39:23 +00:00
cmds = append(cmds, cmd)
}
2020-05-22 02:29:46 +00:00
// 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 {
m.documents[i].Note = msg.Note
}
}
}
2020-05-22 02:29:46 +00:00
switch m.state {
case stashStateReady:
if msg, ok := msg.(boba.KeyMsg); ok {
switch msg.String() {
case "k":
fallthrough
case "up":
m.index--
if m.index < 0 && m.paginator.Page == 0 {
// Stop
m.index = 0
} else if m.index < 0 {
// Go to previous page
m.paginator.PrevPage()
m.index = m.paginator.ItemsOnPage(len(m.documents)) - 1
}
case "j":
fallthrough
case "down":
itemsOnPage := m.paginator.ItemsOnPage(len(m.documents))
m.index++
if m.index >= itemsOnPage && m.paginator.OnLastPage() {
// Stop
m.index = itemsOnPage - 1
} else if m.index >= itemsOnPage {
// Go to next page
m.index = 0
m.paginator.NextPage()
}
// Open document
case "enter":
// Load the document from the server. We'll handle the message
// that comes back in the main update function.
m.state = stashStateLoadingDocument
doc := m.documents[m.markdownIndex()]
2020-05-22 19:31:54 +00:00
cmds = append(cmds,
2020-05-22 19:31:54 +00:00
loadMarkdown(m.cc, doc.ID, doc.markdownType),
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)
}
// Prompt for deletion
case "x":
isUserMarkdown := m.documents[m.markdownIndex()].markdownType == userMarkdown
isValidState := m.state != stashStateSettingNote
if isUserMarkdown && isValidState {
m.state = stashStatePromptDelete
}
}
}
// Update paginator
2020-05-15 00:52:25 +00:00
m.paginator, cmd = paginator.Update(msg, m.paginator)
cmds = append(cmds, cmd)
// Keep the index in bounds when paginating
itemsOnPage := m.paginator.ItemsOnPage(len(m.documents))
if m.index > itemsOnPage-1 {
m.index = itemsOnPage - 1
}
// If we're on the last page and we haven't loaded everything, get
// more stuff.
if m.paginator.OnLastPage() && !m.loading && !m.fullyLoaded {
m.page++
m.loading = true
cmds = append(cmds, loadStash(m))
}
case stashStatePromptDelete:
if msg, ok := msg.(boba.KeyMsg); ok {
switch msg.String() {
// Confirm deletion
case "y":
if m.state != stashStatePromptDelete {
break
}
i := m.markdownIndex()
id := m.documents[i].ID
// Delete optimistically and remove the stashed item
// before we've received a success response.
m.documents = append(m.documents[:i], m.documents[i+1:]...)
// Update pagination
m.paginator.SetTotalPages(len(m.documents))
m.paginator.Page = min(m.paginator.Page, m.paginator.TotalPages-1)
// Set state and delete
m.state = stashStateReady
return m, deleteStashedItem(m.cc, id)
default:
m.state = stashStateReady
}
}
2020-05-22 02:29:46 +00:00
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)
2020-05-15 00:52:25 +00:00
}
2020-05-15 22:34:42 +00:00
// If an item is being confirmed for delete, any key (other than the key
// used for confirmation above) cancels the deletion
2020-05-15 00:52:25 +00:00
return m, boba.Batch(cmds...)
}
// VIEW
func stashView(m stashModel) string {
var s string
2020-05-13 23:02:39 +00:00
switch m.state {
case stashStateInit:
s += spinner.View(m.spinner) + " Loading stash..."
2020-05-15 19:08:45 +00:00
case stashStateLoadingDocument:
2020-05-14 02:08:17 +00:00
s += spinner.View(m.spinner) + " Loading document..."
case stashStateReady:
2020-05-15 22:34:42 +00:00
fallthrough
2020-05-22 02:29:46 +00:00
case stashStateSettingNote:
fallthrough
2020-05-15 22:34:42 +00:00
case stashStatePromptDelete:
2020-05-13 23:02:39 +00:00
if len(m.documents) == 0 {
s += stashEmtpyView(m)
break
}
2020-05-15 00:23:11 +00:00
2020-05-21 19:14:33 +00:00
// We need to fill any empty height with newlines so the footer reaches
// the bottom.
numBlankLines := max(0, (m.terminalHeight-stashViewTopPadding-stashViewBottomPadding)%stashViewItemHeight)
2020-05-15 00:23:11 +00:00
blankLines := ""
if numBlankLines > 0 {
blankLines = strings.Repeat("\n", numBlankLines)
}
2020-05-22 02:29:46 +00:00
header := "Heres your markdown stash:"
switch m.state {
case stashStatePromptDelete:
2020-05-15 22:34:42 +00:00
header = te.String("Delete this item? ").Foreground(common.Red.Color()).String() +
te.String("(y/N)").Foreground(common.FaintRed.Color()).String()
2020-05-22 02:29:46 +00:00
case stashStateSettingNote:
header = te.String("Set the memo for this item?").Foreground(common.YellowGreen.Color()).String()
2020-05-15 22:34:42 +00:00
}
var pagination string
if m.paginator.TotalPages > 1 {
pagination = paginator.View(m.paginator)
if !m.fullyLoaded {
2020-05-18 22:58:19 +00:00
pagination += common.Subtle(" ···")
}
}
2020-05-15 00:52:25 +00:00
s += fmt.Sprintf(
2020-05-15 22:34:42 +00:00
"%s\n\n%s\n\n%s\n\n%s%s\n\n%s",
2020-05-15 01:12:13 +00:00
glowLogoView(" Glow "),
2020-05-15 22:34:42 +00:00
header,
2020-05-15 01:12:13 +00:00
stashPopulatedView(m),
blankLines,
pagination,
2020-05-21 19:14:33 +00:00
stashHelpView(m),
2020-05-15 00:52:25 +00:00
)
2020-05-13 23:02:39 +00:00
}
return "\n" + indent.String(s, 2)
}
func glowLogoView(text string) string {
return te.String(text).
Bold().
Foreground(glowLogoTextColor).
Background(common.Fuschia.Color()).
String()
}
2020-05-13 23:02:39 +00:00
func stashEmtpyView(m stashModel) string {
return "Nothing stashed yet."
}
func stashPopulatedView(m stashModel) string {
2020-05-15 00:52:25 +00:00
var s string
2020-05-15 00:23:11 +00:00
start, end := m.paginator.GetSliceBounds(len(m.documents))
docs := m.documents[start:end]
for i, v := range docs {
2020-05-22 02:29:46 +00:00
state := markdownStateNormal
if i == m.index {
switch m.state {
case stashStatePromptDelete:
state = markdownStateDeleting
case stashStateSettingNote:
state = markdownStateSettingNote
default:
state = markdownStateSelected
}
2020-05-14 00:21:37 +00:00
}
2020-05-22 02:29:46 +00:00
s += stashListItemView(*v).render(m, state) + "\n\n"
2020-05-13 23:02:39 +00:00
}
2020-05-15 00:52:25 +00:00
s = strings.TrimSpace(s) // trim final newlines
// If there aren't enough items to fill up this page (always the last page)
// then we need to add some newlines to fill up the space to push the
// footer stuff down elsewhere.
itemsOnPage := m.paginator.ItemsOnPage(len(m.documents))
if itemsOnPage < m.paginator.PerPage {
n := (m.paginator.PerPage - itemsOnPage) * stashViewItemHeight
2020-05-15 00:52:25 +00:00
s += strings.Repeat("\n", n)
}
return s
2020-05-15 00:23:11 +00:00
}
2020-05-21 19:14:33 +00:00
func stashHelpView(m stashModel) string {
var (
h []string
isNews bool = m.documents[m.markdownIndex()].markdownType == newsMarkdown
)
2020-05-22 02:29:46 +00:00
if m.state == stashStateSettingNote {
h = append(h, "enter: confirm", "esc: cancel")
} else if m.state == stashStatePromptDelete {
2020-05-15 22:34:42 +00:00
h = append(h, "y: delete", "n: cancel")
} else {
h = append(h, "enter: open")
if len(m.documents) > 0 {
h = append(h, "j/k, ↑/↓: choose")
}
if m.paginator.TotalPages > 1 {
h = append(h, "h/l, ←/→: page")
}
2020-05-21 19:14:33 +00:00
if !isNews {
h = append(h, []string{"x: delete"}...)
}
h = append(h, []string{"esc: exit"}...)
2020-05-15 00:23:11 +00:00
}
return common.HelpView(h...)
2020-05-13 23:02:39 +00:00
}
2020-05-22 02:29:46 +00:00
// markdownState is used in a deterministic fashion to aid rendering stash item
// views.
type markdownState int
2020-05-13 23:02:39 +00:00
2020-05-22 02:29:46 +00:00
const (
markdownStateNormal markdownState = iota
markdownStateSelected
markdownStateDeleting
markdownStateSettingNote
)
2020-05-18 20:08:49 +00:00
2020-05-22 02:29:46 +00:00
// stashListItemView contains methods for rendering an item as it appears in
// the stash view
type stashListItemView markdown
func (m stashListItemView) render(mdl stashModel, state markdownState) string {
2020-05-18 20:08:49 +00:00
2020-05-22 02:29:46 +00:00
// General key color
keyColor := common.NoColor
2020-05-22 02:29:46 +00:00
line := common.VerticalLine(common.StateNormal)
switch state {
2020-05-22 02:29:46 +00:00
case markdownStateSettingNote:
keyColor = common.YellowGreen
line = common.VerticalLine(common.StateActive)
case markdownStateSelected:
keyColor = common.Fuschia
2020-05-22 02:29:46 +00:00
line = common.VerticalLine(common.StateSelected)
case markdownStateDeleting:
keyColor = common.Red
2020-05-22 02:29:46 +00:00
line = common.VerticalLine(common.StateDeleting)
2020-05-21 19:14:33 +00:00
default:
if m.markdownType == newsMarkdown {
keyColor = common.Green
}
}
2020-05-18 20:08:49 +00:00
2020-05-21 19:14:33 +00:00
titleKey := "#" + strconv.Itoa(m.ID)
2020-05-22 02:29:46 +00:00
if state == markdownStateSettingNote {
titleKey = textinput.View(mdl.noteInput)
} else {
if m.markdownType == newsMarkdown {
titleKey = "System Announcement"
}
if m.Note != "" {
titleKey += ":"
}
2020-05-14 00:21:37 +00:00
}
2020-05-18 20:08:49 +00:00
2020-05-21 19:14:33 +00:00
dateKey := "Stashed:"
if m.markdownType == newsMarkdown {
dateKey = "Posted:"
}
2020-05-22 02:29:46 +00:00
var titleVal string
if state != markdownStateSettingNote {
titleVal = truncate(m.Note, mdl.terminalWidth-(stashViewHorizontalPadding*2)-len(titleKey))
titleVal = m.title(titleVal, state)
}
2020-05-18 20:08:49 +00:00
2020-05-21 19:14:33 +00:00
titleKey = te.String(titleKey).Foreground(keyColor.Color()).String()
dateKey = te.String(dateKey).Foreground(keyColor.Color()).String()
2020-05-18 20:08:49 +00:00
dateVal := m.date(state)
2020-05-13 23:02:39 +00:00
var s string
2020-05-22 02:29:46 +00:00
s += fmt.Sprintf("%s %s %s\n", line, titleKey, titleVal)
s += fmt.Sprintf("%s %s %s", line, dateKey, dateVal)
2020-05-13 23:02:39 +00:00
return s
}
2020-05-22 02:29:46 +00:00
func (m stashListItemView) date(state markdownState) string {
2020-05-14 00:21:37 +00:00
c := common.Indigo
2020-05-22 02:29:46 +00:00
if state == markdownStateDeleting {
2020-05-14 00:21:37 +00:00
c = common.FaintRed
2020-05-22 02:29:46 +00:00
} else if state == markdownStateSettingNote {
c = dullYellow
2020-05-14 00:21:37 +00:00
}
2020-05-19 00:45:13 +00:00
s := relativeTime(*m.CreatedAt)
2020-05-14 00:21:37 +00:00
return te.String(s).Foreground(c.Color()).String()
2020-05-13 23:02:39 +00:00
}
2020-05-22 02:29:46 +00:00
func (m stashListItemView) title(title string, state markdownState) string {
2020-05-18 20:08:49 +00:00
if title == "" {
2020-05-13 23:02:39 +00:00
return ""
}
2020-05-14 00:21:37 +00:00
c := common.Indigo
2020-05-22 02:29:46 +00:00
if state == markdownStateDeleting {
2020-05-14 00:21:37 +00:00
c = common.Red
}
2020-05-18 20:08:49 +00:00
return te.String(title).Foreground(c.Color()).String()
}
// CMD
2020-05-14 02:08:17 +00:00
func loadStash(m stashModel) boba.Cmd {
return func() boba.Msg {
stash, err := m.cc.GetStash(m.page)
if err != nil {
return errMsg(err)
}
return gotStashMsg(stash)
}
}
2020-05-14 02:08:17 +00:00
2020-05-21 19:14:33 +00:00
func loadNews(m stashModel) boba.Cmd {
2020-05-14 02:08:17 +00:00
return func() boba.Msg {
news, err := m.cc.GetNews(1) // just fetch the first page
2020-05-14 02:08:17 +00:00
if err != nil {
return errMsg(err)
2020-05-14 02:08:17 +00:00
}
return gotNewsMsg(news)
2020-05-21 19:14:33 +00:00
}
}
2020-05-22 19:31:54 +00:00
func loadMarkdown(cc *charm.Client, id int, t markdownType) boba.Cmd {
2020-05-21 19:14:33 +00:00
return func() boba.Msg {
2020-05-22 19:31:54 +00:00
var (
md *charm.Markdown
err error
)
if t == userMarkdown {
md, err = cc.GetStashMarkdown(id)
} else {
md, err = cc.GetNewsMarkdown(id)
}
2020-05-21 19:14:33 +00:00
if err != nil {
return errMsg(err)
2020-05-21 19:14:33 +00:00
}
2020-05-22 19:31:54 +00:00
return fetchedMarkdownMsg(&markdown{
markdownType: userMarkdown,
2020-05-21 19:14:33 +00:00
Markdown: md,
})
2020-05-14 02:08:17 +00:00
}
}
2020-05-15 22:34:42 +00:00
func deleteStashedItem(cc *charm.Client, id int) boba.Cmd {
return func() boba.Msg {
err := cc.DeleteMarkdown(id)
if err != nil {
return errMsg(err)
2020-05-15 22:34:42 +00:00
}
return deletedStashedItemMsg(id)
}
}
2020-05-18 20:08:49 +00:00
// ETC
2020-05-22 02:29:46 +00:00
// wrapMarkdowns wraps a *charm.Markdown with a *markdown in order to add some
// extra metadata.
2020-05-21 19:14:33 +00:00
func wrapMarkdowns(t markdownType, md []*charm.Markdown) (m []*markdown) {
for _, v := range md {
m = append(m, &markdown{
markdownType: t,
Markdown: v,
})
}
return m
}
2020-05-18 20:08:49 +00:00
func truncate(str string, num int) string {
s := str
if len(str) > num {
if num > 1 {
num -= 1
}
s = str[0:num] + "…"
}
return s
}
2020-05-19 00:45:13 +00:00
var magnitudes = []humanize.RelTimeMagnitude{
{D: time.Second, Format: "now", DivBy: time.Second},
{D: 2 * time.Second, Format: "1 second %s", DivBy: 1},
{D: time.Minute, Format: "%d seconds %s", DivBy: time.Second},
{D: 2 * time.Minute, Format: "1 minute %s", DivBy: 1},
{D: time.Hour, Format: "%d minutes %s", DivBy: time.Minute},
{D: 2 * time.Hour, Format: "1 hour %s", DivBy: 1},
{D: humanize.Day, Format: "%d hours %s", DivBy: time.Hour},
{D: 2 * humanize.Day, Format: "1 day %s", DivBy: 1},
{D: humanize.Week, Format: "%d days %s", DivBy: humanize.Day},
{D: 2 * humanize.Week, Format: "1 week %s", DivBy: 1},
{D: humanize.Month, Format: "%d weeks %s", DivBy: humanize.Week},
{D: 2 * humanize.Month, Format: "1 month %s", DivBy: 1},
{D: humanize.Year, Format: "%d months %s", DivBy: humanize.Month},
{D: 18 * humanize.Month, Format: "1 year %s", DivBy: 1},
{D: 2 * humanize.Year, Format: "2 years %s", DivBy: 1},
{D: humanize.LongTime, Format: "%d years %s", DivBy: humanize.Year},
{D: math.MaxInt64, Format: "a long while %s", DivBy: 1},
}
func relativeTime(then time.Time) string {
now := time.Now()
if now.Sub(then) < humanize.Week {
return humanize.CustomRelTime(then, now, "ago", "from now", magnitudes)
}
return then.Format("02 Jan 2006 15:04 MST")
}