diff --git a/go.mod b/go.mod index dce822ff..ad5f6090 100644 --- a/go.mod +++ b/go.mod @@ -28,6 +28,7 @@ require ( github.com/godbus/dbus v4.1.0+incompatible // indirect github.com/google/go-github/v32 v32.1.0 github.com/gophercloud/gophercloud v0.5.0 // indirect + github.com/gorilla/websocket v1.4.0 github.com/hekmon/cunits v2.0.1+incompatible // indirect github.com/hekmon/transmissionrpc v0.0.0-20190525133028-1d589625bacd github.com/imdario/mergo v0.3.8 // indirect diff --git a/modules/finnhub/finnhub.go b/modules/finnhub/finnhub.go new file mode 100644 index 00000000..c4f5d98a --- /dev/null +++ b/modules/finnhub/finnhub.go @@ -0,0 +1,37 @@ +package finnhub + +import ( + "fmt" + "encoding/json" + "github.com/gorilla/websocket" +) + + +func FetchExchangeRates(settings *Settings) (map[string]map[string]float64, error) { + out := map[string]map[string]float64{} + + for base, rates := range settings.rates { + resp, err := http.Get(fmt.Sprintf("https://api.exchangeratesapi.io/latest?base=%s", base)) + if err != nil { + return nil, err + } + defer func() { _ = resp.Body.Close() }() + + var data Response + err = utils.ParseJSON(&data, resp.Body) + if err != nil { + return nil, err + } + + out[base] = map[string]float64{} + + for _, currency := range rates { + rate, ok := data.Rates[currency] + if ok { + out[base][currency] = rate + } + } + } + + return out, nil +} diff --git a/modules/finnhub/settings.go b/modules/finnhub/settings.go new file mode 100644 index 00000000..330ac912 --- /dev/null +++ b/modules/finnhub/settings.go @@ -0,0 +1,52 @@ +package finnhub + +import ( + "github.com/olebedev/config" + "github.com/wtfutil/wtf/cfg" +) + +const ( + defaultFocusable = false + defaultTitle = "Exchange rates" +) + +// Settings defines the configuration properties for this module +type Settings struct { + common *cfg.Common + + precision int `help:"How many decimal places to display." optional:"true"` + + rates map[string][]string `help:"Defines what currency rates we want to know about"` + order []string +} + +// NewSettingsFromYAML creates a new settings instance from a YAML config block +func NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings { + settings := Settings{ + common: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig), + + precision: ymlConfig.UInt("precision", 7), + + rates: map[string][]string{}, + order: []string{}, + } + + raw := ymlConfig.UMap("rates", map[string]interface{}{}) + for key, value := range raw { + settings.order = append(settings.order, key) + settings.rates[key] = []string{} + switch value := value.(type) { + case string: + settings.rates[key] = []string{value} + case []interface{}: + for _, currency := range value { + str, ok := currency.(string) + if ok { + settings.rates[key] = append(settings.rates[key], str) + } + } + } + } + + return &settings +} diff --git a/modules/finnhub/widget.go b/modules/finnhub/widget.go new file mode 100644 index 00000000..1ce623ca --- /dev/null +++ b/modules/finnhub/widget.go @@ -0,0 +1,100 @@ +package finnhub + +import ( + "fmt" + "regexp" + "sort" + + "github.com/rivo/tview" + "github.com/wtfutil/wtf/view" + "github.com/wtfutil/wtf/wtf" +) + +type Widget struct { + view.ScrollableWidget + + settings *Settings + rates map[string]map[string]float64 + err error +} + +func NewWidget(app *tview.Application, pages *tview.Pages, settings *Settings) *Widget { + widget := Widget{ + ScrollableWidget: view.NewScrollableWidget(app, settings.common), + + settings: settings, + } + + widget.SetRenderFunction(widget.Render) + + return &widget +} + +/* -------------------- Exported Functions -------------------- */ + +func (widget *Widget) Refresh() { + + rates, err := FetchExchangeRates(widget.settings) + if err != nil { + widget.err = err + } else { + widget.rates = rates + } + + // The last call should always be to the display function + widget.Render() +} + +func (widget *Widget) Render() { + widget.Redraw(widget.content) +} + +/* -------------------- Unexported Functions -------------------- */ + +func (widget *Widget) content() (string, string, bool) { + if widget.err != nil { + widget.View.SetWrap(true) + return widget.CommonSettings().Title, widget.err.Error(), false + } + + // Sort the bases alphabetically to ensure consistent display ordering + bases := []string{} + for base := range widget.settings.rates { + bases = append(bases, base) + } + sort.Strings(bases) + + out := "" + + for idx, base := range bases { + rates := widget.settings.rates[base] + + rowColor := widget.CommonSettings().RowColor(idx) + + for _, cur := range rates { + rate := widget.rates[base][cur] + + out += fmt.Sprintf( + "[%s]1 %s = %s %s[white]\n", + rowColor, + base, + widget.formatConversionRate(rate), + cur, + ) + + idx++ + } + } + + widget.View.SetWrap(false) + return widget.CommonSettings().Title, out, false +} + +// formatConversionRate takes the raw conversion float and formats it to the precision the +// user specifies in their config (or to the default value) +func (widget *Widget) formatConversionRate(rate float64) string { + rate = wtf.TruncateFloat64(rate, widget.settings.precision) + + r, _ := regexp.Compile(`\.?0*$`) + return r.ReplaceAllString(fmt.Sprintf("%10.7f", rate), "") +}