diff --git a/app/widget_maker.go b/app/widget_maker.go index 784111ec..02d373af 100644 --- a/app/widget_maker.go +++ b/app/widget_maker.go @@ -13,6 +13,7 @@ import ( "github.com/wtfutil/wtf/modules/circleci" "github.com/wtfutil/wtf/modules/clocks" "github.com/wtfutil/wtf/modules/cmdrunner" + "github.com/wtfutil/wtf/modules/covid" "github.com/wtfutil/wtf/modules/cryptoexchanges/bittrex" "github.com/wtfutil/wtf/modules/cryptoexchanges/blockfolio" "github.com/wtfutil/wtf/modules/cryptoexchanges/cryptolive" @@ -137,6 +138,9 @@ func MakeWidget( case "clocks": settings := clocks.NewSettingsFromYAML(moduleName, moduleConfig, config) widget = clocks.NewWidget(tviewApp, settings) + case "covid": + settings := covid.NewSettingsFromYAML(moduleName, moduleConfig, config) + widget = covid.NewWidget(tviewApp, settings) case "cmdrunner": settings := cmdrunner.NewSettingsFromYAML(moduleName, moduleConfig, config) widget = cmdrunner.NewWidget(tviewApp, settings) diff --git a/modules/covid/cases.go b/modules/covid/cases.go new file mode 100644 index 00000000..025d1c7a --- /dev/null +++ b/modules/covid/cases.go @@ -0,0 +1,14 @@ +package covid + +// Cases holds the latest cases +type Cases struct { + Latest Latest `json:"latest"` +} + +// Latest holds the number of global confirmed cases and deaths due to Covid +type Latest struct { + Confirmed int `json:"confirmed"` + Deaths int `json:"deaths"` + // Not currently used but holds information about the country + Locations []interface{} `json:"locations,omitempty"` +} diff --git a/modules/covid/client.go b/modules/covid/client.go new file mode 100644 index 00000000..275efab4 --- /dev/null +++ b/modules/covid/client.go @@ -0,0 +1,58 @@ +package covid + +import ( + "fmt" + "net/http" + + "github.com/wtfutil/wtf/utils" +) + +const covidTrackerAPIURL = "https://coronavirus-tracker-api.herokuapp.com/v2/" + +// LatestCases queries the /latest endpoint, does not take any query parameters +func LatestCases() (*Cases, error) { + latestURL := covidTrackerAPIURL + "latest" + resp, err := http.Get(latestURL) + if resp.StatusCode != 200 { + return nil, fmt.Errorf(resp.Status) + } + if err != nil { + return nil, err + } + defer func() { _ = resp.Body.Close() }() + + var latestGlobalCases Cases + err = utils.ParseJSON(&latestGlobalCases, resp.Body) + if err != nil { + return nil, err + } + + return &latestGlobalCases, nil +} + +// LatestCountryCases queries the /locations endpoint, takes a query parameter: the country code +func (widget *Widget) LatestCountryCases(countries []interface{}) ([]*Cases, error) { + countriesCovidData := []*Cases{} + for _, name := range countries { + countryURL := covidTrackerAPIURL + "locations?source=jhu&country_code=" + name.(string) + resp, err := http.Get(countryURL) + if resp.StatusCode != 200 { + return nil, fmt.Errorf(resp.Status) + } + if err != nil { + return nil, err + } + defer func() { _ = resp.Body.Close() }() + + var latestCountryCases Cases + err = utils.ParseJSON(&latestCountryCases, resp.Body) + if err != nil { + return nil, err + } + // add stats for each country to the slice + countriesCovidData = append(countriesCovidData, &latestCountryCases) + + } + + return countriesCovidData, nil +} diff --git a/modules/covid/client_test.go b/modules/covid/client_test.go new file mode 100644 index 00000000..d6f46770 --- /dev/null +++ b/modules/covid/client_test.go @@ -0,0 +1,31 @@ +package covid + +import ( + "testing" +) + +func TestLatestCases(t *testing.T) { + latestCasesToAssert, err := LatestCases() + if err != nil { + t.Error("LatestCases() returned an error") + } + if latestCasesToAssert.Latest.Confirmed == 0 { + t.Error("LatestCases() should return a non 0 integer") + } +} + +func (widget *Widget) TestCountryCases(t *testing.T) { + countryList := []string{"US", "FR"} + c := make([]interface{}, len(countryList)) + for i, v := range countryList { + c[i] = v + } + latestCountryCasesToAssert, err := widget.LatestCountryCases(c) + if err != nil { + t.Error("LatestCountryCases() returned an error") + } + if len(latestCountryCasesToAssert) == 0 { + t.Error("LatestCountryCases() should not be empty") + } + +} diff --git a/modules/covid/settings.go b/modules/covid/settings.go new file mode 100644 index 00000000..03aec705 --- /dev/null +++ b/modules/covid/settings.go @@ -0,0 +1,31 @@ +package covid + +import ( + "github.com/olebedev/config" + "github.com/wtfutil/wtf/cfg" +) + +const ( + defaultFocusable = false + defaultTitle = "Covid tracker" +) + +// Settings is the struct for this module's settings +type Settings struct { + *cfg.Common + + countries []interface{} `help:"Countries (codes) from which to retrieve stats."` +} + +// NewSettingsFromYAML returns the settings from the config yaml file +func NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings { + + settings := Settings{ + Common: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig), + + // List of countries to retrieve stats from + countries: ymlConfig.UList("countries"), + } + + return &settings +} diff --git a/modules/covid/widget.go b/modules/covid/widget.go new file mode 100644 index 00000000..71199bda --- /dev/null +++ b/modules/covid/widget.go @@ -0,0 +1,87 @@ +package covid + +import ( + "fmt" + + "github.com/rivo/tview" + "github.com/wtfutil/wtf/view" + + "golang.org/x/text/language" + "golang.org/x/text/message" +) + +// Widget is the struct that defines this module widget +type Widget struct { + view.TextWidget + + settings *Settings + err error +} + +// NewWidget creates a new widget for this module +func NewWidget(app *tview.Application, settings *Settings) *Widget { + widget := &Widget{ + TextWidget: view.NewTextWidget(app, nil, settings.Common), + + settings: settings, + } + + widget.View.SetScrollable(true) + + return widget +} + +// Refresh checks if this module widget is disabled +func (widget *Widget) Refresh() { + if widget.Disabled() { + return + } + + widget.Redraw(widget.content) +} + +// Render renders this module widget +func (widget *Widget) Render() { + widget.Redraw(widget.content) +} + +// Display stats based on the user's locale +func (widget *Widget) displayStats(cases int) string { + prntr := message.NewPrinter(language.English) + str := fmt.Sprintf("%s", prntr.Sprintf("%d", cases)) + + return str +} + +func (widget *Widget) content() (string, string, bool) { + title := defaultTitle + if widget.CommonSettings().Title != "" { + title = widget.CommonSettings().Title + } + + cases, err := LatestCases() + var covidStats string + if err != nil { + widget.err = err + } else { + // Display global stats + covidStats = fmt.Sprintf("[%s]Global[white]\n", widget.settings.Colors.Subheading) + covidStats += fmt.Sprintf("%s: %s\n", "Confirmed", widget.displayStats(cases.Latest.Confirmed)) + covidStats += fmt.Sprintf("%s: %s\n", "Deaths", widget.displayStats(cases.Latest.Deaths)) + } + // Retrieve country stats if country codes are set in the config + if len(widget.settings.countries) > 0 { + countryCases, err := widget.LatestCountryCases(widget.settings.countries) + if err != nil { + widget.err = err + } else { + for i, name := range countryCases { + covidStats += fmt.Sprintf("[%s]Country[white]: %s\n", widget.settings.Colors.Subheading, widget.settings.countries[i]) + covidStats += fmt.Sprintf("%s: %s\n", "Confirmed", widget.displayStats(name.Latest.Confirmed)) + covidStats += fmt.Sprintf("%s: %s\n", "Deaths", widget.displayStats(name.Latest.Deaths)) + } + } + } + + return title, covidStats, true +}