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 == 'a': 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 string, success bool) { if success { _ = u.newCard(text, "") } 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, NewContent: 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 }