mirror of
https://github.com/anotherhadi/ilovetui.git
synced 2026-08-21 03:55:48 +02:00
5f99fa7521
Signed-off-by: Hadi <112569860+anotherhadi@users.noreply.github.com>
131 lines
4.2 KiB
Go
131 lines
4.2 KiB
Go
package layout
|
|
|
|
import (
|
|
"charm.land/bubbles/v2/key"
|
|
"charm.land/lipgloss/v2"
|
|
)
|
|
|
|
// HelpProvider is how a pane opts into the help bar: implement it and
|
|
// return whatever bindings you want shown while you're focused. A pane
|
|
// that doesn't implement it just contributes nothing - the help bar still
|
|
// shows layout's own controls (ctrl+hjkl, ?), it never disappears entirely.
|
|
type HelpProvider interface {
|
|
HelpBindings() []key.Binding
|
|
}
|
|
|
|
// HelpBindings implements HelpProvider by delegating to FocusedHelp, so a
|
|
// Model embedded as a HelpProvider behaves identically to one consulted as
|
|
// a Navigable.
|
|
func (m Model) HelpBindings() []key.Binding {
|
|
return m.FocusedHelp()
|
|
}
|
|
|
|
// FocusedHelp implements Navigable: the bindings for whatever pane
|
|
// currently has focus, drilling into a nested Navigable automatically until
|
|
// it reaches the real pane at the bottom. Returns nil if the focused pane
|
|
// (at any depth) implements neither Navigable nor HelpProvider.
|
|
func (m Model) FocusedHelp() []key.Binding {
|
|
focused, ok := findNode(m.root, m.state.id)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
if nav, ok := focused.model.(Navigable); ok {
|
|
return nav.FocusedHelp()
|
|
}
|
|
if hp, ok := focused.model.(HelpProvider); ok {
|
|
return hp.HelpBindings()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// helpKeyMap adapts a focused pane's flat HelpProvider bindings plus
|
|
// layout's own KeyMap into the shape bubbles/help.KeyMap expects.
|
|
type helpKeyMap struct {
|
|
pane []key.Binding
|
|
own KeyMap
|
|
width int
|
|
}
|
|
|
|
// ShortHelp implements help.KeyMap. own leads (its ToggleHelp is always
|
|
// first, see KeyMap.ShortHelp), the focused pane's own bindings follow.
|
|
func (h helpKeyMap) ShortHelp() []key.Binding {
|
|
return append(append([]key.Binding{}, h.own.ShortHelp()...), h.pane...)
|
|
}
|
|
|
|
// FullHelp implements help.KeyMap. Rather than the fixed grouping ShortHelp
|
|
// mirrors, it flattens every binding into one ordered list - own first, so
|
|
// ToggleHelp lands in the first column's first row, then the pane's - and
|
|
// re-flows it into as many columns as fit within width, maximizing columns
|
|
// to minimize the number of rows the full help view takes.
|
|
func (h helpKeyMap) FullHelp() [][]key.Binding {
|
|
all := append(append([]key.Binding{}, flattenGroups(h.own.FullHelp())...), h.pane...)
|
|
return flowColumns(all, h.width)
|
|
}
|
|
|
|
func flattenGroups(groups [][]key.Binding) []key.Binding {
|
|
var flat []key.Binding
|
|
for _, g := range groups {
|
|
flat = append(flat, g...)
|
|
}
|
|
return flat
|
|
}
|
|
|
|
// flowColumns arranges bindings into as many columns as fit within width
|
|
// without overflowing. It fills each column top-to-bottom before moving to
|
|
// the next, which is what bubbles/help.FullHelpView expects: one inner
|
|
// slice per column, rendered as a vertical stack.
|
|
func flowColumns(bindings []key.Binding, width int) [][]key.Binding {
|
|
enabled := make([]key.Binding, 0, len(bindings))
|
|
for _, kb := range bindings {
|
|
if kb.Enabled() {
|
|
enabled = append(enabled, kb)
|
|
}
|
|
}
|
|
if len(enabled) == 0 {
|
|
return nil
|
|
}
|
|
if width <= 0 {
|
|
return [][]key.Binding{enabled}
|
|
}
|
|
|
|
for rows := 1; rows <= len(enabled); rows++ {
|
|
groups := chunkRows(enabled, rows)
|
|
if columnsWidth(groups) <= width {
|
|
return groups
|
|
}
|
|
}
|
|
return [][]key.Binding{enabled}
|
|
}
|
|
|
|
// chunkRows splits bindings into groups of at most rows items each, filling
|
|
// each group before moving to the next - the column-major order help's
|
|
// FullHelpView renders (first group is the leftmost column).
|
|
func chunkRows(bindings []key.Binding, rows int) [][]key.Binding {
|
|
var groups [][]key.Binding
|
|
for i := 0; i < len(bindings); i += rows {
|
|
end := min(i+rows, len(bindings))
|
|
groups = append(groups, bindings[i:end])
|
|
}
|
|
return groups
|
|
}
|
|
|
|
// columnsWidth mirrors bubbles/help.FullHelpView's own width accounting:
|
|
// each column is as wide as its longest key plus a space plus its longest
|
|
// description, columns separated by FullSeparator's width (4 cells, " ").
|
|
func columnsWidth(groups [][]key.Binding) int {
|
|
const separatorWidth = 4
|
|
total := 0
|
|
for i, group := range groups {
|
|
if i > 0 {
|
|
total += separatorWidth
|
|
}
|
|
var keyWidth, descWidth int
|
|
for _, kb := range group {
|
|
keyWidth = max(keyWidth, lipgloss.Width(kb.Help().Key))
|
|
descWidth = max(descWidth, lipgloss.Width(kb.Help().Desc))
|
|
}
|
|
total += keyWidth + 1 + descWidth
|
|
}
|
|
return total
|
|
}
|