package tui import ( "fmt" "strings" "github.com/charmbracelet/lipgloss" "git.unkin.net/unkin/forgebot/pkg/models" ) var columnOrder = []columnDef{ {status: models.StatusTodo, title: "Todo", extra: ""}, {status: models.StatusInProgress, title: "In Progress", extra: ""}, {status: models.StatusInReview, title: "In Review", extra: ""}, {status: models.StatusDone, title: "Done", extra: string(models.StatusWontdo)}, } type columnDef struct { status models.TaskStatus title string extra string } type column struct { def columnDef tasks []models.Task cursor int offset int } type board struct { columns [4]column activeCol int } func newBoard() board { var b board for i, def := range columnOrder { b.columns[i] = column{def: def} } return b } func (b *board) loadTasks(tasks []models.Task) { for i := range b.columns { b.columns[i].tasks = nil } for _, t := range tasks { for i := range b.columns { col := &b.columns[i] if t.Status == col.def.status || string(t.Status) == col.def.extra { col.tasks = append(col.tasks, t) break } } } for i := range b.columns { col := &b.columns[i] if col.cursor >= len(col.tasks) { col.cursor = max(0, len(col.tasks)-1) } } } func (b *board) selectedTask() *models.Task { col := &b.columns[b.activeCol] if len(col.tasks) == 0 { return nil } return &col.tasks[col.cursor] } func (b *board) moveLeft() { if b.activeCol > 0 { b.activeCol-- } } func (b *board) moveRight() { if b.activeCol < len(b.columns)-1 { b.activeCol++ } } func (b *board) moveUp() { col := &b.columns[b.activeCol] if col.cursor > 0 { col.cursor-- } } func (b *board) moveDown() { col := &b.columns[b.activeCol] if col.cursor < len(col.tasks)-1 { col.cursor++ } } func (b *board) view(width, height int) string { colWidth := width / 4 if colWidth < 20 { colWidth = 20 } cardHeight := height - 4 if cardHeight < 1 { cardHeight = 1 } var cols []string for i := range b.columns { cols = append(cols, b.renderColumn(i, colWidth, cardHeight)) } return lipgloss.JoinHorizontal(lipgloss.Top, cols...) } func (b *board) renderColumn(idx, width, maxHeight int) string { col := &b.columns[idx] color := statusColors[col.def.status] active := idx == b.activeCol titleStyle := columnTitleStyle. Width(width). Align(lipgloss.Center). Foreground(color) if active { titleStyle = titleStyle.Underline(true) } title := titleStyle.Render(fmt.Sprintf("%s (%d)", col.def.title, len(col.tasks))) if col.offset > col.cursor { col.offset = col.cursor } var cards []string usedHeight := 0 visibleStart := col.offset for j := visibleStart; j < len(col.tasks); j++ { selected := active && j == col.cursor card := renderCard(col.tasks[j], width, selected) cardLines := strings.Count(card, "\n") + 1 if usedHeight+cardLines > maxHeight && len(cards) > 0 { break } cards = append(cards, card) usedHeight += cardLines } if col.cursor >= visibleStart+len(cards) && len(col.tasks) > 0 { col.offset = col.cursor return b.renderColumn(idx, width, maxHeight) } content := strings.Join(cards, "\n") if len(col.tasks) == 0 { content = dimStyle.Width(width).Align(lipgloss.Center).Render("empty") } scrollInfo := "" if col.offset > 0 { scrollInfo = dimStyle.Render(fmt.Sprintf("↑ %d more", col.offset)) } remaining := len(col.tasks) - visibleStart - len(cards) if remaining > 0 { if scrollInfo != "" { scrollInfo += " " } scrollInfo += dimStyle.Render(fmt.Sprintf("↓ %d more", remaining)) } parts := []string{title} if scrollInfo != "" { parts = append(parts, scrollInfo) } parts = append(parts, content) return lipgloss.JoinVertical(lipgloss.Left, parts...) }