243 lines
4.7 KiB
Go
243 lines
4.7 KiB
Go
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
|
|
}
|
|
|
|
// padHelpCell pads a styled help snippet to cellW visible columns.
|
|
func padHelpCell(part string, cellW int) string {
|
|
pad := cellW - lipgloss.Width(part)
|
|
if pad < 0 {
|
|
pad = 0
|
|
}
|
|
return part + strings.Repeat(" ", pad)
|
|
}
|
|
|
|
// tableHelpParts lays out styled help snippets as a fixed-width column grid.
|
|
func tableHelpParts(parts []string, width int, sep string) []string {
|
|
if len(parts) == 0 {
|
|
return []string{""}
|
|
}
|
|
if width < 1 {
|
|
width = 1
|
|
}
|
|
|
|
cellW := 0
|
|
for _, p := range parts {
|
|
if w := lipgloss.Width(p); w > cellW {
|
|
cellW = w
|
|
}
|
|
}
|
|
if cellW < 1 {
|
|
cellW = 1
|
|
}
|
|
|
|
sepW := lipgloss.Width(sep)
|
|
cols := 1
|
|
if cellW <= width {
|
|
cols = (width + sepW) / (cellW + sepW)
|
|
if cols < 1 {
|
|
cols = 1
|
|
}
|
|
}
|
|
|
|
var lines []string
|
|
for i := 0; i < len(parts); i += cols {
|
|
end := i + cols
|
|
if end > len(parts) {
|
|
end = len(parts)
|
|
}
|
|
row := parts[i:end]
|
|
cells := make([]string, len(row))
|
|
for j, p := range row {
|
|
// Pad every cell except a lone oversized entry on its own row.
|
|
if len(row) == 1 && lipgloss.Width(p) > width {
|
|
cells[j] = p
|
|
} else {
|
|
cells[j] = padHelpCell(p, cellW)
|
|
}
|
|
}
|
|
lines = append(lines, strings.Join(cells, sep))
|
|
}
|
|
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()
|
|
|
|
// Table layout so key/desc columns stay aligned across rows.
|
|
lines := tableHelpParts(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")
|
|
}
|