126 lines
3.2 KiB
Go
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
|
|
}
|