Skip to content

Commit

Permalink
Merge pull request #64 from j0hax/asynchronous-loading
Browse files Browse the repository at this point in the history
Implement asynchronous loading with fancy new status bar
  • Loading branch information
j0hax authored Sep 28, 2023
2 parents 987b123 + a89b4e2 commit 8d650b8
Show file tree
Hide file tree
Showing 6 changed files with 190 additions and 52 deletions.
13 changes: 10 additions & 3 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package app

import (
"github.com/j0hax/go-openmensa"
"github.com/j0hax/mz/app/statusbar"
"github.com/j0hax/mz/config"
"github.com/rivo/tview"
"golang.org/x/text/cases"
Expand All @@ -11,6 +12,8 @@ import (
// errs serves as a delegation for errors
var errs = make(chan error, 1)

var mensas = make(chan string)

// availMenus stores the currently available dates and meals of a canteen
var availMenus []openmensa.Menu

Expand All @@ -29,25 +32,29 @@ var menuList = tview.NewList()
// infoTable shows prices of a selected meal
var infoTable = tview.NewTable()

// titleView displays a text title at the top of the screen
var titleView = tview.NewTextView()
// statusBar displays a small bar at the bottom of the application
var statusBar *statusbar.StatusBar

var cfg *config.Configuration

func StartApp(config *config.Configuration) {
cfg = config

app := tview.NewApplication()

statusBar = statusbar.NewStatusBar(app)

app.EnableMouse(true)

pages := tview.NewPages()

setupLayout(app, pages)
setTitle("mz")

// Display error modal if needed
go errWatcher(app, pages, errs)

go mealLoader(app, mensas)

// Load list of canteens
go loadCanteens(app, mensaList, cfg.Last.Name)

Expand Down
56 changes: 49 additions & 7 deletions app/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"sort"
"strings"
"time"

"github.com/gdamore/tcell/v2"
"github.com/j0hax/go-openmensa"
Expand All @@ -14,6 +15,7 @@ import (
//
// Currently, name and adress are loaded without further configuration.
func loadCanteens(app *tview.Application, list *tview.List, selected string) {
statusBar.StartLoading("all canteens")
mensas, err := openmensa.AllCanteens()
if err != nil {
errs <- err
Expand All @@ -36,6 +38,7 @@ func loadCanteens(app *tview.Application, list *tview.List, selected string) {
mensaList.SetCurrentItem(index)
})
app.QueueEvent(tcell.NewEventKey(tcell.KeyEnter, 0, tcell.ModNone))
statusBar.DoneLoading()
}

// priceSort returns the keys in the ascending order
Expand Down Expand Up @@ -69,6 +72,52 @@ func colorize(cell *tview.TableCell) {
// Todo: find more nice things to colorize
}

// This is the function that loads mensa information asynchonously
func mealLoader(app *tview.Application, mensaName <-chan string) {
for m := range mensaName {
c, err := openmensa.SearchCanteens(m)
if err != nil {
errs <- err
return
}

if len(c) < 1 {
return
}

mensa := c[0]

// Fetch the upcoming Speisepläne
menus, err := mensa.AllMenus()
if err != nil {
errs <- err
} else {
availMenus = menus
}

today := time.Now().Truncate(24 * time.Hour)
for _, menu := range menus {
date := time.Time(menu.Day.Date)
var desc string
if today.Equal(date) {
desc = "Today"
} else {
desc = date.Format("Monday, January 2")
}

app.QueueUpdate(func() {
calendar.AddItem(desc, "", 0, nil)
})
}

if len(menus) > 0 {
cfg.Last.Name = mensa.Name
}

statusBar.DoneLoading()
}
}

// errWatcher waits for an error on ec.
// These errors can be dismissed "ignored," so they should not be used in situations
// where the program can not continue.
Expand Down Expand Up @@ -100,10 +149,3 @@ func errWatcher(app *tview.Application, pages *tview.Pages, ec <-chan error) {
})
}
}

// Sets a cool title at the top of the page
func setTitle(title string) {
wide := strings.Join(strings.Split(title, ""), " ")
t := fmt.Sprintf("[::i]%s[-:-:-]", wide)
titleView.SetText(t)
}
45 changes: 6 additions & 39 deletions app/listeners.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@ package app

import (
"fmt"
"time"

"github.com/j0hax/go-openmensa"
"github.com/rivo/tview"
)

Expand All @@ -19,48 +17,17 @@ var dateIndex int
// If the selected mensa has changed, load its opening dates
func mensaSelected(index int, mainText, secondaryText string, shortcut rune) {
// Fetch canteen
c, err := openmensa.SearchCanteens(mainText)
if err != nil {
errs <- err
return
}

if len(c) < 1 {
return
}

mensa := c[0]

// Fetch the upcoming Speisepläne
menus, err := mensa.AllMenus()
if err != nil {
errs <- err
} else {
availMenus = menus
}

calendar.Clear()
menuList.Clear()
infoTable.Clear()

today := time.Now().Truncate(24 * time.Hour)
for _, menu := range menus {
date := time.Time(menu.Day.Date)
var desc string
if today.Equal(date) {
desc = "Today"
} else {
desc = date.Format("Monday, January 2")
}

calendar.AddItem(desc, "", 0, nil)
select {
case mensas <- mainText:
statusBar.StartLoading(mainText)
default:
// The channel is full: there are too many mensas selected for the goroutine to handle.
// noop.
}

if len(menus) > 0 {
cfg.Last.Name = mensa.Name
}

setTitle(mensa.Name)
}

// If the selected date has changed, load the meals for that date
Expand Down
4 changes: 1 addition & 3 deletions app/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,8 @@ func setupLayout(app *tview.Application, pages *tview.Pages) {
mainView.AddItem(mensaArea, 0, 1, true)
mainView.AddItem(menuArea, 0, 2, false)

titleView.SetTextAlign(tview.AlignCenter).SetDynamicColors(true)

appView.AddItem(titleView, 1, 0, false)
appView.AddItem(mainView, 0, 1, true)
appView.AddItem(statusBar, 1, 0, false)

setupKeybinds(app, mensaArea, menuArea)

Expand Down
46 changes: 46 additions & 0 deletions app/statusbar/primitive.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package statusbar

import (
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
)

// Blur implements tview.Primitive.
func (s *StatusBar) Blur() {
s.field.Blur()
}

// Draw implements tview.Primitive.
func (s *StatusBar) Draw(screen tcell.Screen) {
s.field.Draw(screen)
}

// Focus implements tview.Primitive.
func (s *StatusBar) Focus(delegate func(p tview.Primitive)) {
s.field.Focus(delegate)
}

// GetRect implements tview.Primitive.
func (s *StatusBar) GetRect() (int, int, int, int) {
return s.field.GetRect()
}

// HasFocus implements tview.Primitive.
func (s *StatusBar) HasFocus() bool {
return s.field.HasFocus()
}

// InputHandler implements tview.Primitive.
func (s *StatusBar) InputHandler() func(event *tcell.EventKey, setFocus func(p tview.Primitive)) {
return s.field.InputHandler()
}

// MouseHandler implements tview.Primitive.
func (s *StatusBar) MouseHandler() func(action tview.MouseAction, event *tcell.EventMouse, setFocus func(p tview.Primitive)) (consumed bool, capture tview.Primitive) {
return s.field.MouseHandler()
}

// SetRect implements tview.Primitive.
func (s *StatusBar) SetRect(x int, y int, width int, height int) {
s.field.SetRect(x, y, width, height)
}
78 changes: 78 additions & 0 deletions app/statusbar/statusbar.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package statusbar

import (
"fmt"
"strings"
"sync"
"time"

"github.com/rivo/tview"
)

type StatusBar struct {
app *tview.Application
field *tview.InputField
loadingDone chan bool
mutex sync.Mutex
}

func NewStatusBar(application *tview.Application) *StatusBar {
i := tview.NewInputField()
i.SetDisabled(true)
return &StatusBar{
app: application,
field: i,
loadingDone: make(chan bool),
}
}

func (f *StatusBar) setLabel(s string) {
f.mutex.Lock()
defer f.mutex.Unlock()
f.field.SetLabelWidth(len(s) + 1)
f.field.SetLabel(s)
}

func (f *StatusBar) setMessage(items ...string) {
message := strings.Join(items, " ")

f.mutex.Lock()
defer f.mutex.Unlock()
f.field.SetText(message)
}

// StartLoading displays a loading animation with a specified message.
//
// The animation stops as soon as DoneLoading is called.
func (f *StatusBar) StartLoading(message string) {
f.setLabel("Loading")
go func() {
count := 0
ticker := time.NewTicker(100 * time.Millisecond)
for {
select {
case <-f.loadingDone:
ticker.Stop()
f.setLabel("Finished Loading")
f.setMessage(message)
f.app.Draw()
return
case <-ticker.C:
lbl := fmt.Sprintf("%s%s", message, strings.Repeat(".", count%4))
f.setMessage(lbl)
f.app.Draw()
count += 1
}
}
}()
}

// DoneLoading is called after calling StartLoading to indicate that the loading process has finished.
func (f *StatusBar) DoneLoading() {
select {
case f.loadingDone <- true:
return
default:
panic("Did not call StartLoading()!")
}
}

0 comments on commit 8d650b8

Please sign in to comment.