package ui import ( "fmt" "codeflow.dananglin.me.uk/apollo/pelican/internal/board" "github.com/rivo/tview" ) type ( boardMovement int boardMode string ) const ( next boardMovement = iota previous ) const ( mainPage string = "main" quitPage string = "quit" cardFormPage string = "card form" deleteCardModalPage string = "delete card modal" viewPage string = "view" statusFormPage string = "status form" ) const ( normal boardMode = "NORMAL" selection boardMode = "SELECTION" boardEdit boardMode = "BOARD EDIT" ) type App struct { *tview.Application columns []*column grid *tview.Grid columnFlex *tview.Flex pages *tview.Pages focusedColumn int board board.Board mode boardMode modeView *modeView quitModal *tview.Modal cardForm *cardForm deleteCardModal *tview.Modal statusSelection statusSelection cardView *cardView statusbar *statusbar statusForm *statusForm } // NewApp returns a new App value. func NewApp(path string) (App, error) { kanban, err := board.Open(path) if err != nil { return App{}, fmt.Errorf("unable to open the project's board; %w", err) } app := App{ Application: tview.NewApplication(), pages: tview.NewPages(), grid: tview.NewGrid(), columnFlex: tview.NewFlex(), quitModal: tview.NewModal(), cardForm: newCardForm(), focusedColumn: 0, columns: nil, board: kanban, deleteCardModal: tview.NewModal(), mode: normal, modeView: newModeView(), statusSelection: statusSelection{0, 0, 0}, cardView: newCardView(), statusbar: newStatusbar(), statusForm: newStatusForm(), } return app, nil } // init initialises the UI. func (a *App) Init() error { if err := a.initColumns(); err != nil { return fmt.Errorf("error initialising the status columns; %w", err) } a.columnFlex.SetInputCapture(a.inputCapture()) a.initStatusbar() a.modeView.update(a.mode) a.grid.SetColumns(10, 0).SetRows(0, 1).SetBorders(false) a.grid.AddItem(a.columnFlex, 0, 0, 1, 2, 0, 0, true) a.grid.AddItem(a.modeView, 1, 0, 1, 1, 0, 0, false) a.grid.AddItem(a.statusbar, 1, 1, 1, 1, 0, 0, false) a.pages.AddPage(mainPage, a.grid, true, true) a.initQuitModal() a.pages.AddPage(quitPage, a.quitModal, false, false) a.initCardForm() a.pages.AddPage(cardFormPage, a.cardForm, false, false) a.initDeleteCardModal() a.pages.AddPage(deleteCardModalPage, a.deleteCardModal, false, false) a.initCardView() a.pages.AddPage(viewPage, a.cardView, false, false) a.initStatusForm() a.pages.AddPage(statusFormPage, a.statusForm, false, false) a.SetRoot(a.pages, true) a.refresh(false) return nil } // saveCard saves a new card to the database. func (a *App) saveCard(title, description string) { args := board.CardArgs{ NewTitle: title, NewDescription: description, } _, err := a.board.CreateCard(args) if err != nil { a.statusbar.displayMessage(errorLevel, fmt.Sprintf("Failed to create card: %v.", err)) return } a.statusbar.displayMessage(infoLevel, "Card created successfully.") a.refresh(false) } // editFocusedCard saves an edited card to the database. func (a *App) editFocusedCard(title, description string) { cardID := a.focusedCardID() args := board.UpdateCardArgs{ CardID: cardID, CardArgs: board.CardArgs{ NewTitle: title, NewDescription: description, }, } if err := a.board.UpdateCard(args); err != nil { a.statusbar.displayMessage(errorLevel, fmt.Sprintf("Failed to edit card: %v.", err)) return } a.statusbar.displayMessage(infoLevel, "Card edited successfully.") a.refresh(true) } // getFocusedCard retrieves the details of the card in focus. func (a *App) getFocusedCard() (board.Card, bool) { cardID := a.focusedCardID() card, err := a.board.Card(cardID) if err != nil { a.statusbar.displayMessage( errorLevel, fmt.Sprintf("Failed to retrieve the card with card ID %d, %v.", cardID, err), ) return card, false } return card, true } // deleteFocusedCard deletes the card in focus from the board. func (a *App) deleteFocusedCard() { args := board.DeleteCardArgs{ CardID: a.focusedCardID(), StatusID: a.focusedStatusID(), } if err := a.board.DeleteCard(args); err != nil { a.statusbar.displayMessage(errorLevel, fmt.Sprintf("Failed to delete card: %v.", err)) return } a.statusbar.displayMessage(infoLevel, "Card deleted successfully.") } // updateAllColumns ensures that all columns are up-to-date. func (a *App) updateAllColumns() error { for i := range a.columns { if err := a.updateColumn(i); err != nil { return err } } return nil } // updateColumn ensures that the column is up-to-date. func (a *App) updateColumn(index int) error { if err := a.columns[index].update(a.board); err != nil { return fmt.Errorf("unable to update column; %w", err) } return nil } // shiftColumnFocus shifts the focus to the column to the left or right depending // on the movement set by the user. func (a *App) shiftColumnFocus(movement boardMovement) { switch movement { case next: if a.focusedColumn == len(a.columns)-1 { a.focusedColumn = 0 } else { a.focusedColumn++ } case previous: if a.focusedColumn == 0 { a.focusedColumn = len(a.columns) - 1 } else { a.focusedColumn-- } } a.setColumnFocus() } // setColumnFocus sets the focus to the column primitive as specified by focusedColumn. func (a *App) setColumnFocus() { a.SetFocus(a.columns[a.focusedColumn]) } // refresh refreshes the UI. func (a *App) refresh(updateFocusedColumnOnly bool) { if updateFocusedColumnOnly { if err := a.updateColumn(a.focusedColumn); err != nil { a.statusbar.displayMessage( errorLevel, fmt.Sprintf("Failed to update status column %q: %v", a.focusedStatusName(), err), ) return } } else { if err := a.updateAllColumns(); err != nil { a.statusbar.displayMessage( errorLevel, fmt.Sprintf("Failed to update status columns: %v", err), ) return } } a.setColumnFocus() } // shutdown shuts down the application. func (a *App) shutdown() { a.closeBoard() a.Stop() } // boardMode returns the current board mode. func (a *App) boardMode() boardMode { return a.mode } // updateBoardMode updates the board mode. func (a *App) updateBoardMode(mode boardMode) { a.mode = mode a.modeView.update(mode) } // focusedCardID returns the ID of the card in focus. func (a *App) focusedCardID() int { focusedCard := a.columns[a.focusedColumn].focusedCard id := a.columns[a.focusedColumn].cards[focusedCard].id return id } // focusedStatusID returns the ID of the status (column) in focus. func (a *App) focusedStatusID() int { return a.columns[a.focusedColumn].statusID } // focusedStatusName returns the name of the status (column) in focus. func (a *App) focusedStatusName() string { return a.columns[a.focusedColumn].statusName } // closeBoard closes the board. func (a *App) closeBoard() { _ = a.board.Close() } // editFocusedStatusColumn updates the status column in focus and saves the changes to the database. func (a *App) editFocusedStatusColumn(newName string) { statusID := a.focusedStatusID() args := board.UpdateStatusArgs{ StatusID: statusID, StatusArgs: board.StatusArgs{ Name: newName, }, } if err := a.board.UpdateStatus(args); err != nil { a.statusbar.displayMessage(errorLevel, fmt.Sprintf("Failed to edit status: %v.", err)) return } a.statusbar.displayMessage(infoLevel, "Status updated successfully.") a.refresh(true) }