mirror of
https://github.com/zoriya/bubbles.git
synced 2026-08-05 12:46:14 +00:00
243 lines
6.0 KiB
Go
243 lines
6.0 KiB
Go
package help
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/charmbracelet/bubbles/v2/key"
|
|
tea "github.com/charmbracelet/bubbletea/v2"
|
|
"github.com/charmbracelet/lipgloss/v2"
|
|
)
|
|
|
|
// 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 {
|
|
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
|
|
}
|
|
|
|
// DefaultStyles returns a set of default styles for the help bubble. Light or
|
|
// dark styles can be selected by passing true or false to the isDark
|
|
// parameter.
|
|
func DefaultStyles(isDark bool) Styles {
|
|
lightDark := lipgloss.LightDark(isDark)
|
|
|
|
keyStyle := lipgloss.NewStyle().Foreground(lightDark(lipgloss.Color("#909090"), lipgloss.Color("#626262")))
|
|
descStyle := lipgloss.NewStyle().Foreground(lightDark(lipgloss.Color("#B2B2B2"), lipgloss.Color("#4A4A4A")))
|
|
sepStyle := lipgloss.NewStyle().Foreground(lightDark(lipgloss.Color("#DADADA"), lipgloss.Color("#3C3C3C")))
|
|
|
|
return Styles{
|
|
ShortKey: keyStyle,
|
|
ShortDesc: descStyle,
|
|
ShortSeparator: sepStyle,
|
|
Ellipsis: sepStyle,
|
|
FullKey: keyStyle,
|
|
FullDesc: descStyle,
|
|
FullSeparator: sepStyle,
|
|
}
|
|
}
|
|
|
|
// DefaultDarkStyles returns a set of default styles for dark backgrounds.
|
|
func DefaultDarkStyles() Styles {
|
|
return DefaultStyles(true)
|
|
}
|
|
|
|
// DefaultLightStyles returns a set of default styles for light backgrounds.
|
|
func DefaultLightStyles() Styles {
|
|
return DefaultStyles(false)
|
|
}
|
|
|
|
// Model contains the state of the help view.
|
|
type Model struct {
|
|
Width int
|
|
ShowAll bool // if true, render the "full" help menu
|
|
|
|
ShortSeparator string
|
|
FullSeparator string
|
|
|
|
// The symbol we use in the short help when help items have been truncated
|
|
// due to width. Periods of ellipsis by default.
|
|
Ellipsis string
|
|
|
|
Styles Styles
|
|
}
|
|
|
|
// New creates a new help view with some useful defaults.
|
|
func New() Model {
|
|
return Model{
|
|
ShortSeparator: " • ",
|
|
FullSeparator: " ",
|
|
Ellipsis: "…",
|
|
Styles: DefaultDarkStyles(),
|
|
}
|
|
}
|
|
|
|
// 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).Render(m.ShortSeparator)
|
|
|
|
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).Render(m.FullSeparator)
|
|
)
|
|
|
|
// 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).Render(m.Ellipsis)
|
|
|
|
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
|
|
}
|