Files
superwork/tui/internal/tui/board.go
T
2026-06-23 05:02:15 +08:00

126 lines
3.2 KiB
Go

package tui
import (
"fmt"
"strings"
"github.com/mattn/go-runewidth"
"superwork-tui/internal/issue"
)
// columnOrder defines the fixed display order of kanban columns.
var columnOrder = [4]issue.Column{
issue.ColumnTodo,
issue.ColumnInProgress,
issue.ColumnReview,
issue.ColumnDone,
}
var columnLabels = [4]string{"todo", "in-progress", "review", "done"}
// bucketize distributes issues into 4 ordered buckets matching columnOrder.
// Issues with an unrecognized column fall into ColumnTodo (index 0).
func bucketize(issues []issue.Issue) [4][]issue.Issue {
var buckets [4][]issue.Issue
colIdx := map[issue.Column]int{
issue.ColumnTodo: 0,
issue.ColumnInProgress: 1,
issue.ColumnReview: 2,
issue.ColumnDone: 3,
}
for _, iss := range issues {
idx, ok := colIdx[iss.Column]
if !ok {
idx = 0
}
buckets[idx] = append(buckets[idx], iss)
}
return buckets
}
// cardBadges returns the badge string for a card (e.g. "S P PR#7 B >").
// Uses ASCII only so no emoji dependency issues arise.
func cardBadges(iss issue.Issue) string {
var parts []string
if iss.SpecFile != "" {
parts = append(parts, "S")
}
if iss.PlanFile != "" {
parts = append(parts, "P")
}
if iss.PR != "" {
parts = append(parts, "PR#"+iss.PR)
}
if iss.Branch != "" {
parts = append(parts, "B")
}
if iss.ImplementStatus == "running" {
parts = append(parts, ">")
}
if iss.Prerequisite != 0 {
parts = append(parts, fmt.Sprintf("dep#%d", iss.Prerequisite))
}
return strings.Join(parts, " ")
}
// truncate shortens s to at most n runes, appending "…" when cut.
func truncate(s string, n int) string {
if n <= 0 {
return ""
}
if runewidth.StringWidth(s) <= n {
return s
}
return runewidth.Truncate(s, n, "…")
}
// isBlocked reports whether it is blocked by an unfinished prerequisite.
// Blocked when Prerequisite != 0 and the prerequisite is not in ColumnDone
// (or not found in all — treat missing as unfinished).
func isBlocked(it issue.Issue, all []issue.Issue) bool {
if it.Prerequisite == 0 {
return false
}
for _, candidate := range all {
if candidate.Number == it.Prerequisite {
return candidate.Column != issue.ColumnDone
}
}
return true
}
// todoDepEntry is one row in the rendered todo column dependency tree.
type todoDepEntry struct {
iss issue.Issue
indent bool // true = indented under its prerequisite
}
// todoDepTree orders todo issues so that a blocked card whose prerequisite is
// also in todo appears immediately after that prerequisite, indented one level.
// Cards without a todo-resident blocker appear at the top level in original order.
func todoDepTree(todos []issue.Issue) []todoDepEntry {
todoSet := make(map[int]bool, len(todos))
for _, iss := range todos {
todoSet[iss.Number] = true
}
out := make([]todoDepEntry, 0, len(todos))
emitted := make(map[int]bool, len(todos))
for _, iss := range todos {
if emitted[iss.Number] {
continue
}
out = append(out, todoDepEntry{iss: iss, indent: false})
emitted[iss.Number] = true
for _, child := range todos {
if !emitted[child.Number] && child.Prerequisite == iss.Number {
out = append(out, todoDepEntry{iss: child, indent: true})
emitted[child.Number] = true
}
}
}
return out
}