diff --git a/internal/ui/statusbar.go b/internal/ui/statusbar.go new file mode 100644 index 0000000..9772ae9 --- /dev/null +++ b/internal/ui/statusbar.go @@ -0,0 +1,50 @@ +package ui + +import ( + "fmt" + "time" + + "github.com/rivo/tview" +) + +type statusbarLogLevel int + +const ( + infoLevel statusbarLogLevel = iota + errorLevel +) + +type statusbar struct { + *tview.TextView + duration time.Duration +} + +func newStatusbar() *statusbar { + value := statusbar{ + TextView: tview.NewTextView(), + duration: 5 * time.Second, + } + + value.SetDynamicColors(true).SetBorder(false).SetBorderPadding(0, 0, 1, 1) + + return &value +} + +func (s *statusbar) displayMessage(level statusbarLogLevel, message string) { + go func() { + var colour string + + switch level { + case infoLevel: + colour = "green" + case errorLevel: + colour = "red" + } + + fmt.Fprintf(s, "[%s::b]%s[-:-:-:-]", colour, message) + + time.Sleep(s.duration) + + s.Clear() + }() +} diff --git a/internal/ui/ui.go b/internal/ui/ui.go index a457f83..90df79a 100644 --- a/internal/ui/ui.go +++ b/internal/ui/ui.go @@ -35,7 +35,8 @@ type UI struct { *tview.Application columns []*column - flex *tview.Flex + baseFlex *tview.Flex + columnFlex *tview.Flex pages *tview.Pages focusedColumn int board board.Board @@ -45,6 +46,7 @@ type UI struct { deleteCardModal *tview.Modal statusSelection statusSelection view *cardView + statusbar *statusbar } // NewUI returns a new UI value. @@ -57,7 +59,8 @@ func NewUI(path string) (UI, error) { userInterface := UI{ Application: tview.NewApplication(), pages: tview.NewPages(), - flex: tview.NewFlex(), + baseFlex: tview.NewFlex(), + columnFlex: tview.NewFlex(), quitModal: tview.NewModal(), cardForm: newCardForm(), focusedColumn: 0, @@ -67,6 +70,7 @@ func NewUI(path string) (UI, error) { mode: normal, statusSelection: statusSelection{0, 0, 0}, view: newCardView(), + statusbar: newStatusbar(), } if err := userInterface.init(); err != nil { @@ -83,14 +87,19 @@ func (u *UI) closeBoard() { // init initialises the UI. func (u *UI) init() error { - err := u.initColumns() - if err != nil { + if err := u.initColumns(); err != nil { return fmt.Errorf("error initialising the status columns; %w", err) } - u.flex.SetInputCapture(u.inputCapture()) + u.columnFlex.SetInputCapture(u.inputCapture()) - u.pages.AddPage(mainPage, u.flex, true, true) + u.initStatusbar() + + u.baseFlex.SetDirection(tview.FlexRow) + u.baseFlex.AddItem(u.columnFlex, 0, 1, true) + u.baseFlex.AddItem(u.statusbar, 2, 1, false) + + u.pages.AddPage(mainPage, u.baseFlex, true, true) u.initQuitModal() u.pages.AddPage(quitPage, u.quitModal, false, false) @@ -106,9 +115,7 @@ func (u *UI) init() error { u.SetRoot(u.pages, true) - if err := u.refresh(false); err != nil { - return fmt.Errorf("error refreshing the board; %w", err) - } + u.refresh(false) return nil } @@ -139,7 +146,12 @@ func (u *UI) inputCapture() func(event *tcell.EventKey) *tcell.EventKey { case letter == 'e': if u.mode == normal { u.cardForm.mode = edit - card, _ := u.getFocusedCard() + + card, ok := u.getFocusedCard() + if !ok { + break + } + u.cardForm.updateInputFields(card.Title, card.Description) u.cardForm.frame.SetTitle(" Edit Card ") u.pages.ShowPage(cardFormPage) @@ -147,7 +159,11 @@ func (u *UI) inputCapture() func(event *tcell.EventKey) *tcell.EventKey { } case key == tcell.KeyCtrlD: if u.mode == normal { - card, _ := u.getFocusedCard() + card, ok := u.getFocusedCard() + if !ok { + break + } + text := fmt.Sprintf("Do you want to delete '%s'?", card.Title) u.deleteCardModal.SetText(text) u.pages.ShowPage(deleteCardModalPage) @@ -165,7 +181,11 @@ func (u *UI) inputCapture() func(event *tcell.EventKey) *tcell.EventKey { case key == tcell.KeyEnter: switch u.mode { case normal: - card, _ := u.getFocusedCard() + card, ok := u.getFocusedCard() + if !ok { + break + } + status := u.focusedStatusName() u.view.print(card.ID, card.Title, status, card.Created, card.Description) u.pages.ShowPage(viewPage) @@ -178,7 +198,7 @@ func (u *UI) inputCapture() func(event *tcell.EventKey) *tcell.EventKey { u.statusSelection = statusSelection{0, 0, 0} u.mode = normal - _ = u.refresh(false) + u.refresh(false) } } @@ -192,9 +212,9 @@ func (u *UI) initCardForm() { if success { switch mode { case create: - _ = u.newCard(title, description) + u.saveCard(title, description) case edit: - _ = u.editFocusedCard(title, description) + u.editFocusedCard(title, description) } } @@ -210,7 +230,7 @@ func (u *UI) initDeleteCardModal() { doneFunc := func(_ int, buttonLabel string) { if buttonLabel == "Confirm" { u.deleteFocusedCard() - _ = u.refresh(true) + u.refresh(true) } u.pages.HidePage(deleteCardModalPage) @@ -255,24 +275,35 @@ func (u *UI) initView() { u.view.setDoneFunc(doneFunc) } -// newCard creates and saves a new card to the database. -func (u *UI) newCard(title, description string) error { +func (u *UI) initStatusbar() { + changedFunc := func() { + u.Draw() + } + + u.statusbar.SetChangedFunc(changedFunc) +} + +// saveCard creates and saves a new card to the database. +func (u *UI) saveCard(title, description string) { args := board.CardArgs{ NewTitle: title, NewDescription: description, } - if _, err := u.board.CreateCard(args); err != nil { - return fmt.Errorf("unable to create card, %w", err) + _, err := u.board.CreateCard(args) + if err != nil { + u.statusbar.displayMessage(errorLevel, fmt.Sprintf("Failed to create card: %v.", err)) + + return } - _ = u.refresh(false) + u.statusbar.displayMessage(infoLevel, "Card created successfully.") - return nil + u.refresh(false) } // editFocusedCard saves and edited card to the database. -func (u *UI) editFocusedCard(title, description string) error { +func (u *UI) editFocusedCard(title, description string) { cardID := u.focusedCardID() args := board.UpdateCardArgs{ @@ -284,24 +315,31 @@ func (u *UI) editFocusedCard(title, description string) error { } if err := u.board.UpdateCard(args); err != nil { - return fmt.Errorf("unable to edit card with ID: %d; %w", cardID, err) + u.statusbar.displayMessage(errorLevel, fmt.Sprintf("Failed to edit card: %v.", err)) + + return } - _ = u.refresh(true) + u.statusbar.displayMessage(infoLevel, "Card edited successfully.") - return nil + u.refresh(true) } // getFocusedCard retrieves the details of the focused card. -func (u *UI) getFocusedCard() (board.Card, error) { +func (u *UI) getFocusedCard() (board.Card, bool) { cardID := u.focusedCardID() card, err := u.board.Card(cardID) if err != nil { - return board.Card{}, fmt.Errorf("unable to retrieve the card with card ID %d, %w", cardID, err) + u.statusbar.displayMessage( + errorLevel, + fmt.Sprintf("Failed to retrieve the card with card ID %d, %v.", cardID, err), + ) + + return card, false } - return card, nil + return card, true } // deleteFocusedCard deletes the focused card from the board. @@ -311,12 +349,18 @@ func (u *UI) deleteFocusedCard() { StatusID: u.focusedStatusID(), } - _ = u.board.DeleteCard(args) + if err := u.board.DeleteCard(args); err != nil { + u.statusbar.displayMessage(errorLevel, fmt.Sprintf("Failed to delete card: %v.", err)) + + return + } + + u.statusbar.displayMessage(infoLevel, "Card deleted successfully.") } // initColumns initialises the columns of the Kanban board. func (u *UI) initColumns() error { - u.flex.Clear() + u.columnFlex.Clear() statusList, err := u.board.StatusList() if err != nil { @@ -327,7 +371,7 @@ func (u *UI) initColumns() error { for i := range statusList { column := newColumn(statusList[i].ID, statusList[i].Name, u.boardMode) - u.flex.AddItem(column, 50, 1, true) + u.columnFlex.AddItem(column, 50, 1, true) columns[i] = column } @@ -378,20 +422,28 @@ func (u *UI) setColumnFocus() { } // refresh refreshes the UI. -func (u *UI) refresh(updateFocusedColumnOnly bool) error { +func (u *UI) refresh(updateFocusedColumnOnly bool) { if updateFocusedColumnOnly { if err := u.updateColumn(u.focusedColumn); err != nil { - return err + u.statusbar.displayMessage( + errorLevel, + fmt.Sprintf("Failed to update status column %q: %v", u.focusedStatusName(), err), + ) + + return } } else { if err := u.updateAllColumns(); err != nil { - return err + u.statusbar.displayMessage( + errorLevel, + fmt.Sprintf("Failed to update status columns: %v", err), + ) + + return } } u.setColumnFocus() - - return nil } // shutdown shuts down the application.