Persist comma-separated list filters per project, toggle with space, and jump by first letter in board and project pickers. Co-authored-by: Cursor <cursoragent@cursor.com>
122 lines
2.6 KiB
Go
122 lines
2.6 KiB
Go
package views
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"unicode"
|
|
|
|
"task/internal/ticket"
|
|
)
|
|
|
|
// BoardMenu renders a multi-select board-list picker overlay.
|
|
type BoardMenu struct {
|
|
Cursor int
|
|
Width int
|
|
Styles StatusMenuStyles
|
|
selected map[ticket.BoardList]bool
|
|
}
|
|
|
|
// NewBoardMenu creates a menu with the given filter pre-selected.
|
|
func NewBoardMenu(current ticket.BoardFilter, width int, styles StatusMenuStyles) BoardMenu {
|
|
current = ticket.NormalizeFilter(current)
|
|
sel := map[ticket.BoardList]bool{}
|
|
for _, bl := range current {
|
|
sel[bl] = true
|
|
}
|
|
cursor := 0
|
|
for i, bl := range ticket.AllBoardLists {
|
|
if sel[bl] {
|
|
cursor = i
|
|
break
|
|
}
|
|
}
|
|
return BoardMenu{
|
|
Cursor: cursor,
|
|
Width: width,
|
|
Styles: styles,
|
|
selected: sel,
|
|
}
|
|
}
|
|
|
|
// MoveDown moves the selection down.
|
|
func (m *BoardMenu) MoveDown() {
|
|
if m.Cursor < len(ticket.AllBoardLists)-1 {
|
|
m.Cursor++
|
|
}
|
|
}
|
|
|
|
// MoveUp moves the selection up.
|
|
func (m *BoardMenu) MoveUp() {
|
|
if m.Cursor > 0 {
|
|
m.Cursor--
|
|
}
|
|
}
|
|
|
|
// JumpToShortcut moves the cursor to the list whose label starts with r.
|
|
func (m *BoardMenu) JumpToShortcut(r rune) bool {
|
|
r = unicode.ToLower(r)
|
|
for i, bl := range ticket.AllBoardLists {
|
|
if bl.Shortcut() == r {
|
|
m.Cursor = i
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Toggle flips the checked state of the highlighted list.
|
|
func (m *BoardMenu) Toggle() {
|
|
if m.selected == nil {
|
|
m.selected = map[ticket.BoardList]bool{}
|
|
}
|
|
bl := ticket.AllBoardLists[m.Cursor]
|
|
if bl == ticket.BoardAll {
|
|
m.selected = map[ticket.BoardList]bool{ticket.BoardAll: true}
|
|
return
|
|
}
|
|
delete(m.selected, ticket.BoardAll)
|
|
if m.selected[bl] {
|
|
delete(m.selected, bl)
|
|
} else {
|
|
m.selected[bl] = true
|
|
}
|
|
if len(m.selected) == 0 {
|
|
m.selected[ticket.BoardAll] = true
|
|
}
|
|
}
|
|
|
|
// Selected returns the current multi-select filter.
|
|
func (m BoardMenu) Selected() ticket.BoardFilter {
|
|
var out ticket.BoardFilter
|
|
for _, bl := range ticket.AllBoardLists {
|
|
if m.selected[bl] {
|
|
out = append(out, bl)
|
|
}
|
|
}
|
|
return ticket.NormalizeFilter(out)
|
|
}
|
|
|
|
// View renders the board menu.
|
|
func (m BoardMenu) View() string {
|
|
var lines []string
|
|
lines = append(lines, m.Styles.Title.Render("Filter lists"))
|
|
for i, bl := range ticket.AllBoardLists {
|
|
mark := "[ ]"
|
|
if m.selected[bl] {
|
|
mark = "[x]"
|
|
}
|
|
entry := fmt.Sprintf("%s %c %s", mark, bl.Shortcut(), bl.Label())
|
|
if i == m.Cursor {
|
|
lines = append(lines, m.Styles.Selected.Render("▸ "+entry))
|
|
} else {
|
|
lines = append(lines, m.Styles.Normal.Render(" "+entry))
|
|
}
|
|
}
|
|
body := strings.Join(lines, "\n")
|
|
w := m.Width
|
|
if w < 20 {
|
|
w = 20
|
|
}
|
|
return m.Styles.Box.Width(min(44, w-2)).Render(body)
|
|
}
|