package ui import ( "fmt" "strings" "time" "github.com/charmbracelet/bubbles/table" "tower/internal/model" ) // Column keys, used for future sort expansions. const ( colPri = "Pri" colCat = "Cat" colTitle = "Title" colAge = "Age" colState = "State" ) func newIssueTable() table.Model { cols := []table.Column{ {Title: colPri, Width: 3}, {Title: colCat, Width: 12}, {Title: colTitle, Width: 0}, // widened on resize {Title: colAge, Width: 7}, {Title: colState, Width: 13}, } t := table.New( table.WithColumns(cols), table.WithFocused(true), table.WithHeight(10), ) // Keep built-in styles minimal. s := table.DefaultStyles() s.Header = s.Header.Bold(true) s.Selected = s.Selected.Bold(false) t.SetStyles(s) return t } // BuildRows returns table rows and a parallel issue ID slice (row index -> issue ID). func buildRows(now time.Time, mode AgeMode, issues []model.Issue) ([]table.Row, []string) { rows := make([]table.Row, 0, len(issues)) ids := make([]string, 0, len(issues)) for _, iss := range issues { age := formatAgeWithMode(iss.Age(now), mode) rows = append(rows, table.Row{ iss.Priority.String(), shortCat(iss.Category.String()), oneLine(iss.Title), age, iss.State.String(), }) ids = append(ids, iss.ID) } return rows, ids } func shortCat(cat string) string { if cat == "" { return "-" } if len(cat) <= 12 { return cat } // Keep category compact; table has limited width. s := cat if i := strings.IndexByte(cat, ' '); i > 0 { s = cat[:i] } if len(s) > 12 { return s[:12] } return s } func oneLine(s string) string { s = strings.ReplaceAll(s, "\n", " ") s = strings.TrimSpace(s) return s } func formatAge(d time.Duration) string { return formatAgeWithMode(d, AgeCompact) } func formatAgeWithMode(d time.Duration, mode AgeMode) string { if d <= 0 { if mode == AgeRelative { return "0m ago" } return "0s" } if mode == AgeRelative { // Relative format: Xm ago, Xh ago, Xd ago if d < time.Minute { s := int(d / time.Second) return fmt.Sprintf("%ds ago", s) } if d < time.Hour { m := int(d / time.Minute) return fmt.Sprintf("%dm ago", m) } if d < 24*time.Hour { h := int(d / time.Hour) return fmt.Sprintf("%dh ago", h) } days := int(d / (24 * time.Hour)) return fmt.Sprintf("%dd ago", days) } // Compact format: 0s, Xds, Xdm, Xdh, Xdd if d < time.Minute { s := int(d / time.Second) return fmt.Sprintf("%ds", s) } if d < time.Hour { m := int(d / time.Minute) return fmt.Sprintf("%dm", m) } if d < 24*time.Hour { h := int(d / time.Hour) return fmt.Sprintf("%dh", h) } days := int(d / (24 * time.Hour)) return fmt.Sprintf("%dd", days) }