Make help renderer a little less functional and more structured

This commit is contained in:
Christian Rocha 2020-11-30 22:38:00 -05:00
commit 2eb5f46b01
3 changed files with 321 additions and 294 deletions

View file

@ -69,7 +69,7 @@ func (d DocTypeSet) AsSlice() (agg []DocType) {
return
}
// Return a copy of the given DocumentTypes map.
// Return a copy of the given DoctTypes map.
func copyDocumentTypes(d DocTypeSet) DocTypeSet {
c := make(map[DocType]struct{})
for k, v := range d {

View file

@ -1105,299 +1105,6 @@ func (m stashModel) populatedView() string {
return b.String()
}
func (m stashModel) helpView() (string, int) {
numDocs := len(m.getVisibleMarkdowns())
if m.filterState == filtering {
var h []string
switch numDocs {
case 0:
h = []string{"enter/esc", "cancel"}
case 1:
h = []string{"enter", "open", "esc", "cancel"}
default:
h = []string{"enter", "confirm", "esc", "cancel", "ctrl+j/ctrl+k ↑/↓", "choose"}
}
if m.showFullHelp {
s := m.fullHelpView(h)
l := strings.Count(s, "\n") + 1
return s, l
}
return m.miniHelpView(h...), 1
}
var (
s string
isStashed, isLocal bool
navHelp, filterHelp, selectionHelp, sectionHelp, appHelp []string
)
if numDocs > 0 {
md := m.selectedMarkdown()
isStashed = md != nil && md.markdownType == StashedDoc
isLocal = md != nil && md.markdownType == LocalDoc
}
if m.selectionState == selectionSettingNote {
navHelp = append(navHelp, "enter", "confirm", "esc", "cancel")
appHelp = append(appHelp, "q", "quit")
} else if m.selectionState == selectionPromptingDelete {
selectionHelp = append(selectionHelp, "y", "delete", "n", "cancel")
appHelp = append(appHelp, "q", "quit")
} else {
if numDocs > 0 {
navHelp = append(navHelp, "enter", "open", "j/k ↑/↓", "choose")
}
if m.paginator.TotalPages > 1 {
navHelp = append(navHelp, "h/l ←/→", "page")
}
if m.filterState == filterApplied {
filterHelp = append(filterHelp, "/", "edit filter", "esc", "clear filter")
} else {
filterHelp = append(filterHelp, "/", "filter")
}
if isStashed {
selectionHelp = append(selectionHelp, "x", "delete", "m", "set memo")
} else if isLocal && m.online() {
selectionHelp = append(selectionHelp, "s", "stash")
}
if !m.isFiltering() {
if m.docState == stashShowNewsDocs {
sectionHelp = append(sectionHelp, "n", "home")
} else {
sectionHelp = append(sectionHelp, "n", "news")
}
}
if m.err != nil {
appHelp = append(appHelp, "!", "errors")
}
appHelp = append(appHelp, "q", "quit")
}
if m.showFullHelp {
if m.filterState != filtering && m.selectionState == selectionIdle {
appHelp = append(appHelp, "?", "close help")
}
s = m.fullHelpView(navHelp, filterHelp, selectionHelp, sectionHelp, appHelp)
} else {
if m.filterState != filtering && m.selectionState == selectionIdle {
appHelp = append(appHelp, "?", "more")
}
s = m.miniHelpView(concatStringSlices(
filterHelp,
selectionHelp,
sectionHelp,
appHelp,
)...)
}
return s, strings.Count(s, "\n") + 1
}
// Builds the help view from various sections pieces, truncating it if the view
// would otherwise wrap to two lines. Help view entires should come in as pairs,
// with the first being the key and the second being the help text.
func (m stashModel) miniHelpView(entries ...string) string {
if len(entries) == 0 {
return ""
}
const truncationWidth = 1 // width of "…"
var (
next string
leftGutter = " "
maxWidth = m.general.width -
stashViewHorizontalPadding -
truncationWidth -
ansi.PrintableRuneWidth(leftGutter)
s = leftGutter
)
for i := 0; i < len(entries); i = i + 2 {
k := entries[i]
v := entries[i+1]
switch k {
case "s":
k = greenFg(k)
v = dimGreenFg(v)
default:
k = grayFg(k)
v = midGrayFg(v)
}
next = fmt.Sprintf("%s %s", k, v)
if i < len(entries)-2 {
next += dividerDot
}
// Only this (and the following) help text items if we have the
// horizontal space
if ansi.PrintableRuneWidth(s)+ansi.PrintableRuneWidth(next) >= maxWidth {
s += common.Subtle("…")
break
}
s += next
}
return s
}
func (m stashModel) fullHelpView(cols ...[]string) string {
var (
// Keys and values grouped by column
keys [][]string
vals [][]string
longestCol int
// Final rows grouped by column
assembledCols [][]string
)
// Get key/value pairs
for _, col := range cols {
if len(col) == 0 {
continue // ignore empty columns
}
ks, vs := parseHelpTextPairs(col)
keys = append(keys, ks)
vals = append(vals, vs)
}
// Find the longest column
for _, ks := range keys {
if len(ks) > longestCol {
longestCol = len(ks)
}
}
// Build columns
for i := range keys {
rows := buildHelpTextColumn(keys[i], vals[i], longestCol)
assembledCols = append(assembledCols, rows)
}
// Merge columns
return mergeStashHelpColumns(assembledCols...)
}
// Separate a slice into keys and values. This will panic if it's passed an odd
// number of arguments.
func parseHelpTextPairs(pairs []string) (keys []string, vals []string) {
if len(pairs)%2 != 0 {
panic("help text group must have an even number of items")
}
for i := 0; i < len(pairs); i = i + 2 {
keys = append(keys, pairs[i])
vals = append(vals, pairs[i+1])
}
return
}
// Build rows from keys and values.
func buildHelpTextColumn(keys, vals []string, colHeight int) (rows []string) {
if len(keys) != len(vals) {
panic("help text column keys and vals must be of equal lengths")
}
keyWidth := widestString(keys...)
valWidth := widestString(vals...)
for i := 0; i < colHeight; i++ {
var (
b = strings.Builder{}
k, v string
)
if i < len(keys) {
k = keys[i]
}
if i < len(vals) {
v = vals[i]
}
switch k {
case "s":
k = greenFg(k)
v = dimGreenFg(v)
default:
k = grayFg(k)
v = midGrayFg(v)
}
b.WriteString(k)
b.WriteString(strings.Repeat(" ", keyWidth-ansi.PrintableRuneWidth(k))) // pad keys
b.WriteString(" ") // gap
b.WriteString(v)
b.WriteString(strings.Repeat(" ", valWidth-ansi.PrintableRuneWidth(v))) // pad vals
rows = append(rows, b.String())
}
return
}
// Merge columns together to build the help view.
func mergeStashHelpColumns(cols ...[]string) string {
const minimumHeight = 3
var longestCol int
for _, v := range cols {
n := len(v)
if n > longestCol {
longestCol = n
}
}
if longestCol < minimumHeight {
longestCol = minimumHeight
}
b := strings.Builder{}
for i := 0; i < longestCol; i++ {
for j, col := range cols {
if i >= len(col) {
// Skip if we're past the length of this column
continue
}
if j == 0 {
b.WriteString(" ") // gutter
} else if j > 0 {
b.WriteString(" ") // gap
}
b.WriteString(col[i])
}
if i < longestCol-1 {
b.WriteRune('\n')
}
}
return b.String()
}
func concatStringSlices(s ...[]string) (agg []string) {
for _, v := range s {
agg = append(agg, v...)
}
return
}
// Return the cell width of the widest of the given strings.
func widestString(s ...string) (max int) {
for _, v := range s {
n := ansi.PrintableRuneWidth(v)
if n > max {
max = n
}
}
return
}
// COMMANDS
func loadRemoteMarkdown(cc *charm.Client, id int, t DocType) tea.Cmd {

320
ui/stashhelp.go Normal file
View file

@ -0,0 +1,320 @@
package ui
import (
"fmt"
"strings"
"github.com/charmbracelet/charm/ui/common"
"github.com/muesli/reflow/ansi"
)
// helpEntry is a entry in a help menu containing values for a keystroke and
// it's associated action.
type helpEntry struct{ key, val string }
// helpColumn is a group of helpEntries which will be rendered into a column.
type helpColumn []helpEntry
// newHelpColumn creates a help column from pairs of string arguments
// represeting keys and values. If the arguements are not even (and therein
// not every key has a matching value) the function will panic.
func newHelpColumn(pairs ...string) (h helpColumn) {
if len(pairs)%2 != 0 {
panic("help text group must have an even number of items")
}
for i := 0; i < len(pairs); i = i + 2 {
h = append(h, helpEntry{key: pairs[i], val: pairs[i+1]})
}
return
}
// render returns styled and formatted rows from keys and values.
func (h helpColumn) render(height int) (rows []string) {
keyWidth, valWidth := h.maxWidths()
for i := 0; i < height; i++ {
var (
b = strings.Builder{}
k, v string
)
if i < len(h) {
k = h[i].key
v = h[i].val
switch k {
case "s":
k = greenFg(k)
v = dimGreenFg(v)
default:
k = grayFg(k)
v = midGrayFg(v)
}
}
b.WriteString(k)
b.WriteString(strings.Repeat(" ", keyWidth-ansi.PrintableRuneWidth(k))) // pad keys
b.WriteString(" ") // gap
b.WriteString(v)
b.WriteString(strings.Repeat(" ", valWidth-ansi.PrintableRuneWidth(v))) // pad vals
rows = append(rows, b.String())
}
return
}
// maxWidths returns the widest key and values in the column, respectively.
func (h helpColumn) maxWidths() (maxKey int, maxVal int) {
for _, v := range h {
kw := ansi.PrintableRuneWidth(v.key)
vw := ansi.PrintableRuneWidth(v.val)
if kw > maxKey {
maxKey = kw
}
if vw > maxVal {
maxVal = vw
}
}
return
}
// helpView returns either the mini or full help view depending on the state of
// the model, as well as the total height of the help view.
func (m stashModel) helpView() (string, int) {
numDocs := len(m.getVisibleMarkdowns())
// Help for when we're filtering
if m.filterState == filtering {
var h []string
switch numDocs {
case 0:
h = []string{"enter/esc", "cancel"}
case 1:
h = []string{"enter", "open", "esc", "cancel"}
default:
h = []string{"enter", "confirm", "esc", "cancel", "ctrl+j/ctrl+k ↑/↓", "choose"}
}
return m.renderHelp(h)
}
// Help for when we're interacting with a single document
switch m.selectionState {
case selectionSettingNote:
return m.renderHelp([]string{"enter", "confirm", "esc", "cancel"}, []string{"q", "quit"})
case selectionPromptingDelete:
return m.renderHelp([]string{"y", "delete", "n", "cancel"}, []string{"q", "quit"})
}
var (
isStashed bool
isStashable bool
navHelp []string
filterHelp []string
selectionHelp []string
sectionHelp []string
appHelp []string
)
if numDocs > 0 {
md := m.selectedMarkdown()
isStashed = md != nil && md.markdownType == StashedDoc
isStashable = md != nil && md.markdownType == LocalDoc && m.online()
}
if numDocs > 0 {
navHelp = []string{"enter", "open", "j/k ↑/↓", "choose"}
}
if m.paginator.TotalPages > 1 {
navHelp = append(navHelp, "h/l ←/→", "page")
}
// If we're browsing a filtered set
if m.filterState == filterApplied {
filterHelp = []string{"/", "edit filter", "esc", "clear filter"}
} else {
filterHelp = []string{"/", "filter"}
}
if isStashed {
selectionHelp = []string{"x", "delete", "m", "set memo"}
} else if isStashable {
selectionHelp = []string{"s", "stash"}
}
// If there's no filtering happening
if !m.isFiltering() {
if m.docState == stashShowNewsDocs {
sectionHelp = []string{"n", "home"}
} else {
sectionHelp = []string{"n", "news"}
}
}
// If there are errors
if m.err != nil {
appHelp = append(appHelp, "!", "errors")
}
appHelp = append(appHelp, "q", "quit")
// Detailed help
if m.showFullHelp {
if m.filterState != filtering {
appHelp = append(appHelp, "?", "close help")
}
return m.renderHelp(navHelp, filterHelp, selectionHelp, sectionHelp, appHelp)
}
// Mini help
if m.filterState != filtering {
appHelp = append(appHelp, "?", "more")
}
return m.renderHelp(filterHelp, selectionHelp, sectionHelp, appHelp)
}
// renderHelp returns the rendered help view and associated line height for
// the given groups of help items.
func (m stashModel) renderHelp(groups ...[]string) (string, int) {
if m.showFullHelp {
str := m.fullHelpView(groups...)
numLines := strings.Count(str, "\n") + 1
return str, numLines
}
return m.miniHelpView(concatStringSlices(groups...)...), 1
}
// Builds the help view from various sections pieces, truncating it if the view
// would otherwise wrap to two lines. Help view entires should come in as pairs,
// with the first being the key and the second being the help text.
func (m stashModel) miniHelpView(entries ...string) string {
if len(entries) == 0 {
return ""
}
var (
truncationChar = common.Subtle("…")
truncationWidth = ansi.PrintableRuneWidth(truncationChar)
)
var (
next string
leftGutter = " "
maxWidth = m.general.width -
stashViewHorizontalPadding -
truncationWidth -
ansi.PrintableRuneWidth(leftGutter)
s = leftGutter
)
for i := 0; i < len(entries); i = i + 2 {
k := entries[i]
v := entries[i+1]
switch k {
case "s":
k = greenFg(k)
v = dimGreenFg(v)
default:
k = grayFg(k)
v = midGrayFg(v)
}
next = fmt.Sprintf("%s %s", k, v)
if i < len(entries)-2 {
next += dividerDot
}
// Only this (and the following) help text items if we have the
// horizontal space
if ansi.PrintableRuneWidth(s)+ansi.PrintableRuneWidth(next) >= maxWidth {
s += truncationChar
break
}
s += next
}
return s
}
func (m stashModel) fullHelpView(groups ...[]string) string {
var (
columns []helpColumn
tallestCol int
renderedCols [][]string // final rows grouped by column
)
// Get key/value pairs
for _, g := range groups {
if len(g) == 0 {
continue // ignore empty columns
}
columns = append(columns, newHelpColumn(g...))
}
// Find the tallest column
for _, c := range columns {
if len(c) > tallestCol {
tallestCol = len(c)
}
}
// Build columns
for _, c := range columns {
renderedCols = append(renderedCols, c.render(tallestCol))
}
// Merge columns
return mergeColumns(renderedCols...)
}
// Merge columns together to build the help view.
func mergeColumns(cols ...[]string) string {
const minimumHeight = 3
// Find the tallest column
var tallestCol int
for _, v := range cols {
n := len(v)
if n > tallestCol {
tallestCol = n
}
}
// Make sure the tallest column meets the minimum height
if tallestCol < minimumHeight {
tallestCol = minimumHeight
}
b := strings.Builder{}
for i := 0; i < tallestCol; i++ {
for j, col := range cols {
if i >= len(col) {
continue // skip if we're past the length of this column
}
if j == 0 {
b.WriteString(" ") // gutter
} else if j > 0 {
b.WriteString(" ") // gap
}
b.WriteString(col[i])
}
if i < tallestCol-1 {
b.WriteRune('\n')
}
}
return b.String()
}
func concatStringSlices(s ...[]string) (agg []string) {
for _, v := range s {
agg = append(agg, v...)
}
return
}