package ui import ( "strings" "github.com/charmbracelet/lipgloss" ) type helpEntry struct { key string desc string } func (m Model) helpEntries() []helpEntry { switch m.mode { case ModeCreate: entries := []helpEntry{ {"enter", "save"}, {"esc", "cancel"}, } if !m.creatingProject { entries = append([]helpEntry{{"ctrl+e", "editor"}}, entries...) } return entries case ModeSearch: return []helpEntry{ {"enter", "apply"}, {"esc", "cancel"}, } case ModeProjects: return []helpEntry{ {"j/k", "navigate"}, {"a-z", "jump"}, {"enter", "select"}, {"n", "new project"}, {"e", "edit"}, {"Y", "Sync"}, {"x", "delete"}, {"X", "force delete"}, {"esc", "cancel"}, } case ModeEditProject: return []helpEntry{ {"tab/↑↓", "fields"}, {"enter", "save"}, {"esc", "cancel"}, } case ModeMoveTicket: return []helpEntry{ {"j/k", "navigate"}, {"a-z", "jump"}, {"enter", "move here"}, {"esc", "cancel"}, } case ModeStatus: return []helpEntry{ {"j/k", "select"}, {"o/w/d", "jump"}, {"enter", "apply"}, {"esc", "cancel"}, } case ModeSync: return []helpEntry{ {"j/k", "select"}, {"p/u", "jump"}, {"enter", "run"}, {"esc", "cancel"}, } case ModeBoard: return []helpEntry{ {"j/k", "navigate"}, {"a/b/i/d", "jump"}, {"space", "toggle"}, {"enter", "apply"}, {"esc", "cancel"}, } case ModeConfirmDelete: return []helpEntry{ {"y/enter", "confirm"}, {"esc", "cancel"}, } case ModeTimeManual: return []helpEntry{ {"enter", "save"}, {"esc", "cancel"}, } case ModeTimeEntries: return []helpEntry{ {"j/k", "navigate"}, {"e", "edit times"}, {"x", "delete"}, {"u/U", "undo/redo"}, {"esc", "back"}, } case ModeTimeEdit: return []helpEntry{ {"tab/↑↓", "fields"}, {"enter", "save"}, {"esc", "cancel"}, } case ModeReport: return []helpEntry{ {"g", "group"}, {"j/k", "scroll"}, {"esc", "back"}, } case ModeReportGroup: return []helpEntry{ {"j/k", "select"}, {"p/s/t/d", "jump"}, {"enter", "apply"}, {"esc", "cancel"}, } default: return []helpEntry{ {"j/k", "navigate"}, {"J/K", "checklist"}, {"S-↑/↓", "checklist"}, {"space", "check"}, {"n", "new"}, {"e", "edit"}, {"t/T", "timer/log"}, {"m", "entries"}, {"r", "report"}, {"Y", "Sync"}, {"u/U", "undo/redo"}, {"/", "search"}, {"p", "project"}, {"P", "move"}, {"l", "list"}, {"s", "status"}, {"x", "delete"}, {"X", "force delete"}, {"q", "quit"}, } } } func (m Model) helpParts() []string { parts := make([]string, 0, len(m.helpEntries())) for _, e := range m.helpEntries() { parts = append(parts, m.styles.HelpKey.Render(e.key)+" "+m.styles.HelpDesc.Render(e.desc)) } return parts } func (m Model) helpSep() string { sep := m.styles.HelpSep.String() if sep == "" { return " " } return sep } // wrapHelpParts packs styled help snippets into lines that fit within width. func wrapHelpParts(parts []string, width int, sep string) []string { if width < 1 { width = 1 } sepW := lipgloss.Width(sep) var lines []string var cur string for _, p := range parts { pw := lipgloss.Width(p) if cur == "" { cur = p // Oversized single entry still gets its own line so nothing is dropped. if pw > width { lines = append(lines, cur) cur = "" } continue } if lipgloss.Width(cur)+sepW+pw <= width { cur = cur + sep + p continue } lines = append(lines, cur) cur = p if pw > width { lines = append(lines, cur) cur = "" } } if cur != "" { lines = append(lines, cur) } if len(lines) == 0 { return []string{""} } return lines } func (m Model) renderFooter() string { width := m.width if width <= 0 { width = 80 } ver := m.styles.Version.Render(m.version) verW := lipgloss.Width(ver) sep := m.helpSep() parts := m.helpParts() // Wrap help across the full width so all bindings stay visible. lines := wrapHelpParts(parts, width, sep) // Place version on the last line to the right when it fits; otherwise // put it on an extra right-aligned line. last := lines[len(lines)-1] gap := width - lipgloss.Width(last) - verW if gap >= 1 { lines[len(lines)-1] = last + strings.Repeat(" ", gap) + ver } else { pad := width - verW if pad < 0 { pad = 0 } lines = append(lines, strings.Repeat(" ", pad)+ver) } return strings.Join(lines, "\n") }