Files
LazyPM/pkg/tui/components/helpbar.go
Robin Olsen d900a5fcd3 Merge pull request #75 from LazyBachelor/LPM-140
LPM-140 The Sprinting Update
Adds sprint support across the web UI (board + issue detail), TUI kanban, CLI/REPL, and the Beads-backed storage layer.

Changes:

Introduces sprint entities in storage and exposes sprint operations via IssueService.
Updates web board to support backlog vs. sprint columns, sprint selection, and sprint creation routes.
Updates TUI kanban to show backlog + sprint columns and adds sprint selection/creation actions.
2026-03-23 19:26:04 +01:00

253 lines
5.6 KiB
Go

package components
import (
"strings"
"charm.land/lipgloss/v2"
"github.com/LazyBachelor/LazyPM/internal/style"
)
type ViewKind int
const (
ViewIssues ViewKind = iota
ViewKanban
)
const (
fullHelpKeyWidth = 10
fullHelpDescWidth = 18
fullHelpColGap = 4
maxFullHelpCols = 4
)
type HelpItem struct {
Key string
Desc string
}
type HelpBarConfig struct {
ShortItems []HelpItem
FullItems []HelpItem
}
type HelpBar struct {
view ViewKind
config HelpBarConfig
showAll bool
width int
}
func NewHelpBar(view ViewKind) HelpBar {
return HelpBar{
view: view,
config: helpBarConfig(view),
}
}
func (h *HelpBar) SetWidth(width int) {
h.width = width
}
func (h HelpBar) View() string {
if h.width == 0 {
return ""
}
if h.showAll {
return h.fullHelp()
}
return h.shortHelp()
}
func (h HelpBar) shortHelp() string {
items := make([]string, 0, len(h.config.ShortItems))
for _, item := range h.config.ShortItems {
items = append(
items,
style.HighlightKey(item.Key)+item.Desc+" ",
)
}
content := lipgloss.JoinHorizontal(lipgloss.Left, items...)
return lipgloss.NewStyle().
Border(lipgloss.Border{Top: "─"}, true, false, false, false).
BorderForeground(style.SecondaryBorder).
Padding(0, 1).
Width(h.width).
Render(content)
}
func (h HelpBar) fullHelp() string {
if len(h.config.FullItems) == 0 {
return ""
}
keyStyle := lipgloss.NewStyle().Width(fullHelpKeyWidth).Align(lipgloss.Right)
descStyle := lipgloss.NewStyle().Width(fullHelpDescWidth)
cellStyle := lipgloss.NewStyle().Width(fullHelpKeyWidth + 1 + fullHelpDescWidth)
renderItem := func(item HelpItem) string {
return cellStyle.Render(
keyStyle.Render(style.HighlightKey(item.Key)) + " " + descStyle.Render(item.Desc),
)
}
innerWidth := max(h.width-2, 1)
cellWidth := fullHelpKeyWidth + 1 + fullHelpDescWidth
cols := fitHelpColumns(innerWidth, cellWidth, fullHelpColGap, maxFullHelpCols)
rows := (len(h.config.FullItems) + cols - 1) / cols
gap := strings.Repeat(" ", fullHelpColGap)
var result []string
for r := range rows {
var cells []string
for c := range cols {
if idx := r + c*rows; idx < len(h.config.FullItems) {
cells = append(cells, renderItem(h.config.FullItems[idx]))
}
}
result = append(result, joinHorizontalWithGap(cells, gap))
}
content := lipgloss.JoinVertical(lipgloss.Left, result...)
return lipgloss.NewStyle().
Border(lipgloss.Border{Top: "─"}, true, false, false, false).
BorderForeground(style.SecondaryBorder).
Padding(0, 1).
Width(h.width).
Render(content)
}
func (h HelpBar) Height() int {
if h.width == 0 {
return 0
}
return lipgloss.Height(h.View())
}
func (h HelpBar) IsExpanded() bool {
return h.showAll
}
func (h *HelpBar) ToggleHelp() {
h.showAll = !h.showAll
}
func fitHelpColumns(
availableWidth int,
cellWidth int,
gapWidth int,
maxCols int,
) int {
for cols := maxCols; cols >= 1; cols-- {
neededWidth := cols*cellWidth + (cols-1)*gapWidth
if neededWidth <= availableWidth {
return cols
}
}
return 1
}
func joinHorizontalWithGap(cells []string, gap string) string {
if len(cells) == 0 {
return ""
}
parts := make([]string, 0, len(cells)*2-1)
for i, cell := range cells {
if i > 0 {
parts = append(parts, gap)
}
parts = append(parts, cell)
}
return lipgloss.JoinHorizontal(lipgloss.Left, parts...)
}
func helpBarConfig(view ViewKind) HelpBarConfig {
switch view {
case ViewIssues:
return HelpBarConfig{
ShortItems: []HelpItem{
{Key: "v", Desc: "kanban"},
{Key: "↑/k", Desc: "up"},
{Key: "↓/j", Desc: "down"},
{Key: "/", Desc: "search"},
{Key: "a", Desc: "add"},
{Key: "c", Desc: "comment"},
{Key: "e/d/s/p/t/A", Desc: "edit"},
{Key: "x", Desc: "delete"},
{Key: "q", Desc: "quit"},
{Key: "?", Desc: "help"},
},
FullItems: []HelpItem{
{Key: "v", Desc: "kanban"},
{Key: "↑/k", Desc: "up"},
{Key: "↓/j", Desc: "down"},
{Key: "/", Desc: "search"},
{Key: "a", Desc: "add issue"},
{Key: "c", Desc: "add comment"},
{Key: "x", Desc: "delete issue"},
{Key: "e", Desc: "edit title"},
{Key: "d", Desc: "edit description"},
{Key: "s", Desc: "change status"},
{Key: "p", Desc: "change priority"},
{Key: "t", Desc: "change type"},
{Key: "A", Desc: "change assignee"},
{Key: "q", Desc: "quit"},
{Key: "?", Desc: "help"},
},
}
case ViewKanban:
return HelpBarConfig{
ShortItems: []HelpItem{
{Key: "v", Desc: "list view"},
{Key: "↑/k", Desc: "up"},
{Key: "↓/j", Desc: "down"},
{Key: "/", Desc: "search"},
{Key: "n", Desc: "new sprint"},
{Key: "S", Desc: "select sprint"},
{Key: "pgup/pgdn", Desc: "page"},
{Key: "h/l", Desc: "column"},
{Key: "←/→", Desc: "move"},
{Key: "a", Desc: "add"},
{Key: "e/d/s/p/t/A/", Desc: "edit"},
{Key: "x", Desc: "delete"},
{Key: "q", Desc: "quit"},
{Key: "?", Desc: "help"},
},
FullItems: []HelpItem{
{Key: "v", Desc: "list view"},
{Key: "h/l", Desc: "switch column"},
{Key: "←/→", Desc: "move issue"},
{Key: "↑/k", Desc: "up"},
{Key: "↓/j", Desc: "down"},
{Key: "/", Desc: "search"},
{Key: "pgup", Desc: "page up"},
{Key: "pgdn", Desc: "page down"},
{Key: "a", Desc: "add issue"},
{Key: "x", Desc: "delete issue"},
{Key: "e", Desc: "edit title"},
{Key: "d", Desc: "edit description"},
{Key: "s", Desc: "change status"},
{Key: "p", Desc: "change priority"},
{Key: "t", Desc: "change type"},
{Key: "A", Desc: "change assignee"},
{Key: "S", Desc: "select sprint"},
{Key: "n", Desc: "new sprint"},
{Key: "q", Desc: "quit"},
{Key: "?", Desc: "help"},
},
}
default:
return HelpBarConfig{}
}
}