package ui import ( "fmt" "codeflow.dananglin.me.uk/apollo/pelican/internal/board" "github.com/gdamore/tcell/v2" "github.com/rivo/tview" ) type card struct { id int wrappedTitle []string created string height int } type column struct { *tview.Box statusID int statusName string cards []card focusedCard int getBoardModeFunc func() boardMode } func newColumn(statusID int, statusName string, getBoardModeFunc func() boardMode) *column { column := column{ Box: tview.NewBox(), statusID: statusID, statusName: statusName, cards: nil, focusedCard: 0, getBoardModeFunc: getBoardModeFunc, } column.SetBorder(true) return &column } func (c *column) addCard(cardID int, title, created string) { wrappedTitle := tview.WordWrap(title, 40) metadataHeight := 1 height := len(wrappedTitle) + metadataHeight card := card{ id: cardID, wrappedTitle: wrappedTitle, created: created, height: height, } c.cards = append(c.cards, card) } func (c *column) shiftCardFocus(movement boardMovement) { numCards := len(c.cards) switch movement { case next: if c.focusedCard == numCards-1 { c.focusedCard = 0 } else { c.focusedCard++ } case previous: if c.focusedCard == 0 { c.focusedCard = numCards - 1 } else { c.focusedCard-- } } } func (c *column) clear() { c.cards = nil c.focusedCard = 0 } func (c *column) update(kanban board.Board) error { c.clear() status, err := kanban.Status(c.statusID) if err != nil { return fmt.Errorf("unable to retrieve details about Status %d from the database; %w", c.statusID, err) } numCardIDs := len(status.CardIds) if status.CardIds != nil && numCardIDs > 0 { cards, err := kanban.CardList(status.CardIds) if err != nil { return fmt.Errorf("unable to get the list of cards from Status %d; %w", c.statusID, err) } for _, card := range cards { c.addCard(card.ID, card.Title, card.Created) } } c.statusName = status.Name c.SetTitle(fmt.Sprintf(" %s (%d) ", c.statusName, numCardIDs)) return nil } // InputHandler returns the handler for this primitive. func (c *column) InputHandler() func(event *tcell.EventKey, setFocus func(p tview.Primitive)) { return c.WrapInputHandler(func(event *tcell.EventKey, _ func(p tview.Primitive)) { key, letter := event.Key(), event.Rune() switch { case key == tcell.KeyDown || letter == 'j': c.shiftCardFocus(next) case key == tcell.KeyUp || letter == 'k': c.shiftCardFocus(previous) } }) } func (c *column) Draw(screen tcell.Screen) { var mode boardMode // Update border before calling c.DrawForSubclass() if c.getBoardModeFunc == nil { mode = normal } else { mode = c.getBoardModeFunc() } if mode == selection { if c.HasFocus() { c.SetBorderColor(tcell.ColorTurquoise.TrueColor()) } else { c.SetBorderColor(tcell.ColorOrangeRed.TrueColor()) } } else { c.SetBorderColor(tcell.ColorOrangeRed.TrueColor()) } c.DrawForSubclass(screen, c) x, y, width, _ := c.GetInnerRect() xOffset := x + 1 cursor := y gap := 1 for ind := 0; ind < len(c.cards); ind++ { var ( style tcell.Style vertLine rune ) if c.HasFocus() && ind == c.focusedCard && mode == normal { vertLine = tview.BoxDrawingsHeavyVertical style = tcell.StyleDefault.Foreground(tcell.ColorBlue.TrueColor()) } else { vertLine = tview.BoxDrawingsLightVertical style = tcell.StyleDefault.Foreground(tcell.ColorYellow.TrueColor()) } // Draw a vertical line down the left side of each card for cy := cursor; cy < cursor+c.cards[ind].height; cy++ { screen.SetContent(xOffset, cy, vertLine, nil, style) } // Print the card's title for j := range c.cards[ind].wrappedTitle { tview.Print( screen, c.cards[ind].wrappedTitle[j], xOffset+2, cursor+j, width, tview.AlignLeft|tview.AlignTop, tcell.ColorGreen.TrueColor(), ) } // Print the card's metadata cal := "📅" cardTextFmt := "#%d %s%s" tview.Print( screen, fmt.Sprintf(cardTextFmt, c.cards[ind].id, cal, c.cards[ind].created), xOffset+2, cursor+len(c.cards[ind].wrappedTitle), width, tview.AlignLeft|tview.AlignTop, tcell.ColorGrey.TrueColor(), ) cursor = cursor + c.cards[ind].height + gap } // draw a 'placeholder card' if in status selection board mode if mode == selection && c.HasFocus() { vertLine := tview.BoxDrawingsHeavyVertical style := tcell.StyleDefault.Foreground(tcell.ColorWhite.TrueColor()) for cy := cursor; cy < cursor+1; cy++ { screen.SetContent(xOffset, cy, vertLine, nil, style) } tview.Print( screen, "Move the card to this column", xOffset+2, cursor, width, tview.AlignLeft|tview.AlignTop, tcell.ColorWhite.TrueColor(), ) } }