pelican/internal/ui/ui.go
Dan Anglin f956b7da59
refactor: change content field to description
Change the content field to description for the card type in preparation
for supporting card notes.
2024-01-10 12:12:54 +00:00

322 lines
6.8 KiB
Go

package ui
import (
"fmt"
"codeflow.dananglin.me.uk/apollo/pelican/internal/board"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
)
type (
boardMovement int
boardMode int
)
const (
next boardMovement = iota
previous
)
const (
normal boardMode = iota
selection
)
const (
mainPage string = "main"
quitPage string = "quit"
addPage string = "add"
deleteCardPage string = "delete card"
)
type UI struct {
*tview.Application
columns []*column
flex *tview.Flex
pages *tview.Pages
focusedColumn int
board board.Board
mode boardMode
quitModal *tview.Modal
addModal *modalInput
deleteCardModal *tview.Modal
statusSelection statusSelection
}
// NewUI returns a new UI value.
func NewUI(path string) (UI, error) {
kanban, err := board.Open(path)
if err != nil {
return UI{}, fmt.Errorf("unable to open the project's board; %w", err)
}
userInterface := UI{
Application: tview.NewApplication(),
pages: tview.NewPages(),
flex: tview.NewFlex(),
quitModal: tview.NewModal(),
addModal: newModalInput(),
focusedColumn: 0,
columns: nil,
board: kanban,
deleteCardModal: tview.NewModal(),
mode: normal,
statusSelection: statusSelection{0, 0, 0},
}
if err := userInterface.init(); err != nil {
return UI{}, fmt.Errorf("received an error after running the initialisation; %w", err)
}
return userInterface, nil
}
// closeBoard closes the board.
func (u *UI) closeBoard() {
_ = u.board.Close()
}
// init initialises the UI.
func (u *UI) init() error {
err := u.initColumns()
if err != nil {
return fmt.Errorf("error initialising the status columns; %w", err)
}
u.flex.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
key, letter := event.Key(), event.Rune()
switch {
case letter == 'h' || key == tcell.KeyLeft:
u.shiftColumnFocus(previous)
case letter == 'l' || key == tcell.KeyRight:
u.shiftColumnFocus(next)
case letter == 'c':
if u.mode == normal {
u.pages.ShowPage(addPage)
u.SetFocus(u.addModal)
}
case letter == 'm':
if u.mode == normal {
focusedCard := u.columns[u.focusedColumn].focusedCard
u.statusSelection.cardID = u.columns[u.focusedColumn].cards[focusedCard].id
u.statusSelection.currentStatusID = u.columns[u.focusedColumn].statusID
u.mode = selection
}
case key == tcell.KeyCtrlD:
if u.mode == normal {
u.pages.ShowPage(deleteCardPage)
u.SetFocus(u.deleteCardModal)
}
case key == tcell.KeyCtrlQ:
if u.mode == normal {
u.pages.ShowPage(quitPage)
u.SetFocus(u.quitModal)
}
case key == tcell.KeyESC:
if u.mode != normal {
u.mode = normal
}
case key == tcell.KeyEnter:
if u.mode == selection {
u.statusSelection.nextStatusID = u.columns[u.focusedColumn].statusID
if u.statusSelection.currentStatusID != u.statusSelection.nextStatusID {
u.statusSelection.moveCard(u.board)
}
u.statusSelection = statusSelection{0, 0, 0}
u.mode = normal
_ = u.refresh(false)
}
}
return event
})
u.pages.AddPage(mainPage, u.flex, true, true)
u.initQuitModal()
u.pages.AddPage(quitPage, u.quitModal, false, false)
u.initAddInputModal()
u.pages.AddPage(addPage, u.addModal, false, false)
u.initDeleteCardModal()
u.pages.AddPage(deleteCardPage, u.deleteCardModal, false, false)
u.SetRoot(u.pages, true)
if err := u.refresh(false); err != nil {
return fmt.Errorf("error refreshing the board; %w", err)
}
return nil
}
// initAddInputModal initialises the add input modal.
func (u *UI) initAddInputModal() {
doneFunc := func(text, description string, success bool) {
if success {
_ = u.newCard(text, description)
}
u.pages.HidePage(addPage)
u.setColumnFocus()
}
u.addModal.SetDoneFunc(doneFunc)
}
// initDeleteCardModal initialises the modal for deleting cards.
func (u *UI) initDeleteCardModal() {
doneFunc := func(_ int, buttonLabel string) {
if buttonLabel == "Confirm" {
u.deleteCard()
_ = u.refresh(true)
}
u.pages.HidePage(deleteCardPage)
u.setColumnFocus()
}
u.deleteCardModal.SetText("Do you want to delete this card?").
AddButtons([]string{"Confirm", "Cancel"}).
SetDoneFunc(doneFunc)
}
// deleteCard deletes a card from the board.
func (u *UI) deleteCard() {
statusID := u.columns[u.focusedColumn].statusID
focusedCard := u.columns[u.focusedColumn].focusedCard
cardID := u.columns[u.focusedColumn].cards[focusedCard].id
args := board.DeleteCardArgs{
CardID: cardID,
StatusID: statusID,
}
_ = u.board.DeleteCard(args)
}
// initQuitModal initialises the quit modal.
func (u *UI) initQuitModal() {
doneFunc := func(_ int, buttonLabel string) {
switch buttonLabel {
case "Quit":
u.shutdown()
default:
u.pages.HidePage(quitPage)
u.setColumnFocus()
}
}
u.quitModal.SetText("Do you want to quit the application?").
AddButtons([]string{"Quit", "Cancel"}).
SetDoneFunc(doneFunc)
}
// newCard creates and saves a new card to the database.
func (u *UI) newCard(title, content string) error {
args := board.CardArgs{
NewTitle: title,
NewDescription: content,
}
if _, err := u.board.CreateCard(args); err != nil {
return fmt.Errorf("unable to create card, %w", err)
}
_ = u.refresh(false)
return nil
}
func (u *UI) initColumns() error {
u.flex.Clear()
statusList, err := u.board.StatusList()
if err != nil {
return fmt.Errorf("unable to retrieve the list of statuses; %w", err)
}
columns := make([]*column, len(statusList))
for i := range statusList {
column := newColumn(statusList[i].ID, statusList[i].Name, u.boardMode)
u.flex.AddItem(column, 50, 1, true)
columns[i] = column
}
u.columns = columns
return nil
}
func (u *UI) updateAllColumns() error {
for i := range u.columns {
if err := u.updateColumn(i); err != nil {
return err
}
}
return nil
}
func (u *UI) updateColumn(index int) error {
if err := u.columns[index].update(u.board); err != nil {
return fmt.Errorf("unable to update column; %w", err)
}
return nil
}
func (u *UI) shiftColumnFocus(movement boardMovement) {
switch movement {
case next:
if u.focusedColumn == len(u.columns)-1 {
u.focusedColumn = 0
} else {
u.focusedColumn++
}
case previous:
if u.focusedColumn == 0 {
u.focusedColumn = len(u.columns) - 1
} else {
u.focusedColumn--
}
}
u.setColumnFocus()
}
func (u *UI) setColumnFocus() {
u.SetFocus(u.columns[u.focusedColumn])
}
// refresh refreshes the UI.
func (u *UI) refresh(updateFocusedColumnOnly bool) error {
if updateFocusedColumnOnly {
if err := u.updateColumn(u.focusedColumn); err != nil {
return err
}
} else {
if err := u.updateAllColumns(); err != nil {
return err
}
}
u.setColumnFocus()
return nil
}
// shutdown shuts down the application.
func (u *UI) shutdown() {
u.closeBoard()
u.Stop()
}
func (u *UI) boardMode() boardMode {
return u.mode
}