Wire manual Y menu and CLI sync for issues and work items, persist per-project credentials, and keep local edits until an explicit push. Co-authored-by: Cursor <cursoragent@cursor.com>
224 lines
4.3 KiB
Go
224 lines
4.3 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", "YouTrack"},
|
|
{"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"},
|
|
{"x", "delete"},
|
|
{"u/U", "undo/redo"},
|
|
{"esc", "back"},
|
|
}
|
|
case ModeTimeEdit:
|
|
return []helpEntry{
|
|
{"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", "YouTrack"},
|
|
{"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")
|
|
}
|