feat(ui): add a status bar #25

Manually merged
dananglin merged 2 commits from 19-statusbar into main 2024-01-13 12:39:50 +00:00
2 changed files with 139 additions and 37 deletions

50
internal/ui/statusbar.go Normal file
View file

@ -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()
}()
}

View file

@ -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.