mirror of
https://github.com/zoriya/bubbles.git
synced 2026-08-05 20:56:17 +00:00
248 lines
6.2 KiB
Go
248 lines
6.2 KiB
Go
package help
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/charmbracelet/bubbles/v2/key"
|
|
tea "github.com/charmbracelet/bubbletea/v2"
|
|
"github.com/charmbracelet/lipgloss"
|
|
)
|
|
|
|
// KeyMap is a map of keybindings used to generate help. Since it's an
|
|
// interface it can be any type, though struct or a map[string][]key.Binding
|
|
// are likely candidates.
|
|
//
|
|
// Note that if a key is disabled (via key.Binding.SetEnabled) it will not be
|
|
// rendered in the help view, so in theory generated help should self-manage.
|
|
type KeyMap interface {
|
|
// ShortHelp returns a slice of bindings to be displayed in the short
|
|
// version of the help. The help bubble will render help in the order in
|
|
// which the help items are returned here.
|
|
ShortHelp() []key.Binding
|
|
|
|
// FullHelp returns an extended group of help items, grouped by columns.
|
|
// The help bubble will render the help in the order in which the help
|
|
// items are returned here.
|
|
FullHelp() [][]key.Binding
|
|
}
|
|
|
|
// Styles is a set of available style definitions for the Help bubble.
|
|
type Styles struct {
|
|
// The symbol we use in the short help when help items have been truncated
|
|
// due to width. Periods of ellipsis by default.
|
|
Ellipsis lipgloss.Style
|
|
|
|
// Styling for the short help
|
|
ShortKey lipgloss.Style
|
|
ShortDesc lipgloss.Style
|
|
ShortSeparator lipgloss.Style
|
|
|
|
// Styling for the full help
|
|
FullKey lipgloss.Style
|
|
FullDesc lipgloss.Style
|
|
FullSeparator lipgloss.Style
|
|
}
|
|
|
|
// BaseStyles returns a set of styles with layouts, but no colors.
|
|
func BaseStyles() Styles {
|
|
return Styles{
|
|
Ellipsis: lipgloss.NewStyle().SetString("…"),
|
|
ShortSeparator: lipgloss.NewStyle().SetString(" • "),
|
|
FullSeparator: lipgloss.NewStyle().SetString(" "),
|
|
}
|
|
}
|
|
|
|
// DefaultStylesDark returns a set of dark background styles for help.
|
|
func DefaultStylesDark() Styles {
|
|
var (
|
|
s = BaseStyles()
|
|
keyColor = lipgloss.Color("#626262")
|
|
descColor = lipgloss.Color("#4A4A4A")
|
|
sepColor = lipgloss.Color("#3C3C3C")
|
|
)
|
|
|
|
s.ShortKey = s.ShortKey.Foreground(keyColor)
|
|
s.ShortDesc = s.ShortDesc.Foreground(descColor)
|
|
s.ShortSeparator = s.ShortSeparator.Foreground(sepColor)
|
|
s.FullKey = s.FullKey.Foreground(keyColor)
|
|
s.FullDesc = s.FullDesc.Foreground(descColor)
|
|
s.FullSeparator = s.FullSeparator.Foreground(sepColor)
|
|
return s
|
|
}
|
|
|
|
// DefaultStylesLight returns a set of light background styles for help.
|
|
func DefaultStylesLight() Styles {
|
|
var (
|
|
s = BaseStyles()
|
|
keyColor = lipgloss.Color("#909090")
|
|
descColor = lipgloss.Color("#B2B2B2")
|
|
sepColor = lipgloss.Color("#DDDADA")
|
|
)
|
|
|
|
s.ShortKey = s.ShortKey.Foreground(keyColor)
|
|
s.ShortDesc = s.ShortDesc.Foreground(descColor)
|
|
s.ShortSeparator = s.ShortSeparator.Foreground(sepColor)
|
|
s.FullKey = s.FullKey.Foreground(keyColor)
|
|
s.FullDesc = s.FullDesc.Foreground(descColor)
|
|
s.FullSeparator = s.FullSeparator.Foreground(sepColor)
|
|
return s
|
|
}
|
|
|
|
// Model contains the state of the help view.
|
|
type Model struct {
|
|
Width int
|
|
ShowAll bool // if true, render the "full" help menu
|
|
Styles Styles
|
|
}
|
|
|
|
// New creates a new help view with some useful defaults.
|
|
func New() Model {
|
|
return Model{
|
|
Styles: DefaultStylesDark(),
|
|
}
|
|
}
|
|
|
|
// Update helps satisfy the Bubble Tea Model interface. It's a no-op.
|
|
func (m Model) Update(_ tea.Msg) (Model, tea.Cmd) {
|
|
return m, nil
|
|
}
|
|
|
|
// View renders the help view's current state.
|
|
func (m Model) View(k KeyMap) string {
|
|
if m.ShowAll {
|
|
return m.FullHelpView(k.FullHelp())
|
|
}
|
|
return m.ShortHelpView(k.ShortHelp())
|
|
}
|
|
|
|
// ShortHelpView renders a single line help view from a slice of keybindings.
|
|
// If the line is longer than the maximum width it will be gracefully
|
|
// truncated, showing only as many help items as possible.
|
|
func (m Model) ShortHelpView(bindings []key.Binding) string {
|
|
if len(bindings) == 0 {
|
|
return ""
|
|
}
|
|
|
|
var b strings.Builder
|
|
var totalWidth int
|
|
separator := m.Styles.ShortSeparator.Inline(true).String()
|
|
|
|
for i, kb := range bindings {
|
|
if !kb.Enabled() {
|
|
continue
|
|
}
|
|
|
|
// Sep
|
|
var sep string
|
|
if totalWidth > 0 && i < len(bindings) {
|
|
sep = separator
|
|
}
|
|
|
|
// Item
|
|
str := sep +
|
|
m.Styles.ShortKey.Inline(true).Render(kb.Help().Key) + " " +
|
|
m.Styles.ShortDesc.Inline(true).Render(kb.Help().Desc)
|
|
w := lipgloss.Width(str)
|
|
|
|
// Tail
|
|
if tail, ok := m.shouldAddItem(totalWidth, w); !ok {
|
|
if tail != "" {
|
|
b.WriteString(tail)
|
|
}
|
|
break
|
|
}
|
|
|
|
totalWidth += w
|
|
b.WriteString(str)
|
|
}
|
|
|
|
return b.String()
|
|
}
|
|
|
|
// FullHelpView renders help columns from a slice of key binding slices. Each
|
|
// top level slice entry renders into a column.
|
|
func (m Model) FullHelpView(groups [][]key.Binding) string {
|
|
if len(groups) == 0 {
|
|
return ""
|
|
}
|
|
|
|
// Linter note: at this time we don't think it's worth the additional
|
|
// code complexity involved in preallocating this slice.
|
|
//nolint:prealloc
|
|
var (
|
|
out []string
|
|
|
|
totalWidth int
|
|
separator = m.Styles.FullSeparator.Inline(true).String()
|
|
)
|
|
|
|
// Iterate over groups to build columns
|
|
for i, group := range groups {
|
|
if group == nil || !shouldRenderColumn(group) {
|
|
continue
|
|
}
|
|
var (
|
|
sep string
|
|
keys []string
|
|
descriptions []string
|
|
)
|
|
|
|
// Sep
|
|
if totalWidth > 0 && i < len(groups) {
|
|
sep = separator
|
|
}
|
|
|
|
// Separate keys and descriptions into different slices
|
|
for _, kb := range group {
|
|
if !kb.Enabled() {
|
|
continue
|
|
}
|
|
keys = append(keys, kb.Help().Key)
|
|
descriptions = append(descriptions, kb.Help().Desc)
|
|
}
|
|
|
|
// Column
|
|
col := lipgloss.JoinHorizontal(lipgloss.Top,
|
|
sep,
|
|
m.Styles.FullKey.Render(strings.Join(keys, "\n")),
|
|
" ",
|
|
m.Styles.FullDesc.Render(strings.Join(descriptions, "\n")),
|
|
)
|
|
w := lipgloss.Width(col)
|
|
|
|
// Tail
|
|
if tail, ok := m.shouldAddItem(totalWidth, w); !ok {
|
|
if tail != "" {
|
|
out = append(out, tail)
|
|
}
|
|
break
|
|
}
|
|
|
|
totalWidth += w
|
|
out = append(out, col)
|
|
}
|
|
|
|
return lipgloss.JoinHorizontal(lipgloss.Top, out...)
|
|
}
|
|
|
|
func (m Model) shouldAddItem(totalWidth, width int) (tail string, ok bool) {
|
|
// If there's room for an ellipsis, print that.
|
|
if m.Width > 0 && totalWidth+width > m.Width {
|
|
tail = " " + m.Styles.Ellipsis.Inline(true).String()
|
|
|
|
if totalWidth+lipgloss.Width(tail) < m.Width {
|
|
return tail, false
|
|
}
|
|
}
|
|
return "", true
|
|
}
|
|
|
|
func shouldRenderColumn(b []key.Binding) (ok bool) {
|
|
for _, v := range b {
|
|
if v.Enabled() {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|