mirror of
https://github.com/charmbracelet/glow.git
synced 2026-08-20 23:24:18 +02:00
Make help renderer a little less functional and more structured
This commit is contained in:
parent
c235780eee
commit
2eb5f46b01
3 changed files with 321 additions and 294 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
293
ui/stash.go
293
ui/stash.go
|
|
@ -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
320
ui/stashhelp.go
Normal 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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue