2020-12-01 03:38:00 +00:00
|
|
|
package ui
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
"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
|
2023-03-03 02:42:12 +00:00
|
|
|
// representing keys and values. If the arguments are not even (and therein
|
2020-12-01 03:38:00 +00:00
|
|
|
// 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)
|
2020-12-17 23:31:08 +00:00
|
|
|
v = semiDimGreenFg(v)
|
2020-12-01 03:38:00 +00:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
|
|
|
var (
|
2023-01-19 19:13:41 +00:00
|
|
|
isEditable bool
|
2020-12-01 03:38:00 +00:00
|
|
|
navHelp []string
|
|
|
|
filterHelp []string
|
|
|
|
selectionHelp []string
|
2023-01-19 19:13:41 +00:00
|
|
|
editHelp []string
|
2020-12-01 03:38:00 +00:00
|
|
|
sectionHelp []string
|
|
|
|
appHelp []string
|
|
|
|
)
|
|
|
|
|
2020-12-03 23:15:59 +00:00
|
|
|
if numDocs > 0 && m.showFullHelp {
|
2020-12-01 03:38:00 +00:00
|
|
|
navHelp = []string{"enter", "open", "j/k ↑/↓", "choose"}
|
|
|
|
}
|
|
|
|
|
2020-12-03 23:15:59 +00:00
|
|
|
if len(m.sections) > 1 {
|
|
|
|
if m.showFullHelp {
|
|
|
|
navHelp = append(navHelp, "tab/shift+tab", "section")
|
|
|
|
} else {
|
|
|
|
navHelp = append(navHelp, "tab", "section")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-12-08 23:38:10 +00:00
|
|
|
if m.paginator().TotalPages > 1 {
|
2020-12-01 03:38:00 +00:00
|
|
|
navHelp = append(navHelp, "h/l ←/→", "page")
|
|
|
|
}
|
|
|
|
|
|
|
|
// If we're browsing a filtered set
|
2024-07-03 15:11:29 +00:00
|
|
|
if m.filterApplied() {
|
|
|
|
filterHelp = []string{"/", "edit search", "esc", "clear filter"}
|
2020-12-01 03:38:00 +00:00
|
|
|
} else {
|
2020-12-15 00:53:06 +00:00
|
|
|
filterHelp = []string{"/", "find"}
|
2020-12-01 03:38:00 +00:00
|
|
|
}
|
|
|
|
|
2023-01-19 19:13:41 +00:00
|
|
|
if isEditable {
|
|
|
|
editHelp = []string{"e", "edit"}
|
|
|
|
}
|
|
|
|
|
2020-12-01 03:38:00 +00:00
|
|
|
// If there are errors
|
|
|
|
if m.err != nil {
|
|
|
|
appHelp = append(appHelp, "!", "errors")
|
|
|
|
}
|
|
|
|
|
2024-07-08 19:15:08 +00:00
|
|
|
appHelp = append(appHelp, "r", "refresh")
|
2020-12-01 03:38:00 +00:00
|
|
|
appHelp = append(appHelp, "q", "quit")
|
|
|
|
|
|
|
|
// Detailed help
|
|
|
|
if m.showFullHelp {
|
|
|
|
if m.filterState != filtering {
|
|
|
|
appHelp = append(appHelp, "?", "close help")
|
|
|
|
}
|
2023-01-19 19:13:41 +00:00
|
|
|
return m.renderHelp(navHelp, filterHelp, append(selectionHelp, editHelp...), sectionHelp, appHelp)
|
2020-12-01 03:38:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Mini help
|
|
|
|
if m.filterState != filtering {
|
|
|
|
appHelp = append(appHelp, "?", "more")
|
|
|
|
}
|
2023-01-19 19:13:41 +00:00
|
|
|
return m.renderHelp(navHelp, filterHelp, selectionHelp, editHelp, sectionHelp, appHelp)
|
2020-12-01 03:38:00 +00:00
|
|
|
}
|
|
|
|
|
2024-07-03 15:11:29 +00:00
|
|
|
const minHelpViewHeight = 5
|
|
|
|
|
2020-12-01 03:38:00 +00:00
|
|
|
// 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
|
2024-07-03 15:11:29 +00:00
|
|
|
return str, max(numLines, minHelpViewHeight)
|
2020-12-01 03:38:00 +00:00
|
|
|
}
|
|
|
|
return m.miniHelpView(concatStringSlices(groups...)...), 1
|
|
|
|
}
|
|
|
|
|
|
|
|
// Builds the help view from various sections pieces, truncating it if the view
|
2023-03-03 02:42:12 +00:00
|
|
|
// would otherwise wrap to two lines. Help view entries should come in as pairs,
|
2020-12-01 03:38:00 +00:00
|
|
|
// 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 (
|
2021-06-14 20:52:00 +00:00
|
|
|
truncationChar = subtleStyle.Render("…")
|
2020-12-01 03:38:00 +00:00
|
|
|
truncationWidth = ansi.PrintableRuneWidth(truncationChar)
|
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
next string
|
|
|
|
leftGutter = " "
|
2020-12-11 01:43:31 +00:00
|
|
|
maxWidth = m.common.width -
|
2020-12-01 03:38:00 +00:00
|
|
|
stashViewHorizontalPadding -
|
|
|
|
truncationWidth -
|
|
|
|
ansi.PrintableRuneWidth(leftGutter)
|
|
|
|
s = leftGutter
|
|
|
|
)
|
|
|
|
|
|
|
|
for i := 0; i < len(entries); i = i + 2 {
|
|
|
|
k := entries[i]
|
|
|
|
v := entries[i+1]
|
|
|
|
|
2024-07-03 15:11:29 +00:00
|
|
|
k = grayFg(k)
|
|
|
|
v = midGrayFg(v)
|
2020-12-01 03:38:00 +00:00
|
|
|
|
|
|
|
next = fmt.Sprintf("%s %s", k, v)
|
|
|
|
|
|
|
|
if i < len(entries)-2 {
|
2023-05-05 09:40:36 +00:00
|
|
|
next += dividerDot.String()
|
2020-12-01 03:38:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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 {
|
2022-10-25 14:40:51 +00:00
|
|
|
var tallestCol int
|
|
|
|
columns := make([]helpColumn, 0, len(groups))
|
|
|
|
renderedCols := make([][]string, 0, len(groups)) // final rows grouped by column
|
2020-12-01 03:38:00 +00:00
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|