From 9cc10ccdd00bd8b03f034c89dba7d74bba862d92 Mon Sep 17 00:00:00 2001 From: Simon Roberts Date: Thu, 18 Nov 2021 14:15:04 +1100 Subject: [PATCH 1/3] Implement alt-link and bind it to ^o --- cointop/cointop.go | 9 +++++-- cointop/config.go | 13 ++++++++++ cointop/default_shortcuts.go | 1 + cointop/dominance.go | 4 +-- cointop/keybindings.go | 2 ++ cointop/price.go | 4 +-- cointop/table.go | 20 +++++++++++++++ cointop/util.go | 7 ++++++ pkg/api/api.go | 11 ++++---- pkg/api/impl/coingecko/coingecko.go | 26 +++++++++++++++++-- pkg/api/impl/coinmarketcap/coinmarketcap.go | 28 ++++++++++++++++++--- pkg/api/interface.go | 1 + pkg/eval/eval.go | 20 +++++++++++++++ 13 files changed, 130 insertions(+), 16 deletions(-) diff --git a/cointop/cointop.go b/cointop/cointop.go index fa47dda..57ca795 100644 --- a/cointop/cointop.go +++ b/cointop/cointop.go @@ -95,6 +95,7 @@ type State struct { favoritesCompactNotation bool portfolioCompactNotation bool enableMouse bool + altCoinLinkCode string } // Cointop cointop @@ -195,6 +196,9 @@ var DefaultCompactNotation = false // DefaultEnableMouse ... var DefaultEnableMouse = true +// DefaultAltCoinLinkCode +var DefaultAltCoinLinkCode = "sprintf(\"https://www.tradingview.com/chart/?symbol=%sUSDT\", coin.Symbol)" + // DefaultMaxChartWidth ... var DefaultMaxChartWidth = 175 @@ -302,6 +306,7 @@ func NewCointop(config *Config) (*Cointop, error) { }, compactNotation: DefaultCompactNotation, enableMouse: DefaultEnableMouse, + altCoinLinkCode: DefaultAltCoinLinkCode, tableCompactNotation: DefaultCompactNotation, favoritesCompactNotation: DefaultCompactNotation, portfolioCompactNotation: DefaultCompactNotation, @@ -412,9 +417,9 @@ func NewCointop(config *Config) (*Cointop, error) { } if ct.apiChoice == CoinMarketCap { - ct.api = api.NewCMC(ct.apiKeys.cmc) + ct.api = api.NewCMC(ct.apiKeys.cmc, ct.State.altCoinLinkCode) } else if ct.apiChoice == CoinGecko { - ct.api = api.NewCG(perPage, maxPages) + ct.api = api.NewCG(perPage, maxPages, ct.State.altCoinLinkCode) } else { return nil, ErrInvalidAPIChoice } diff --git a/cointop/config.go b/cointop/config.go index 40b7af3..a156eb8 100644 --- a/cointop/config.go +++ b/cointop/config.go @@ -50,6 +50,7 @@ type ConfigFileConfig struct { CacheDir interface{} `toml:"cache_dir"` CompactNotation interface{} `toml:"compact_notation"` EnableMouse interface{} `toml:"enable_mouse"` + AltCoinLinkCode interface{} `toml:"alt_link_code"` // TODO: should really be in API-specific section Table map[string]interface{} `toml:"table"` Chart map[string]interface{} `toml:"chart"` } @@ -74,6 +75,7 @@ func (ct *Cointop) SetupConfig() error { ct.loadCacheDirFromConfig, ct.loadCompactNotationFromConfig, ct.loadEnableMouseFromConfig, + ct.loadAltCoinLinkCodeFromConfig, ct.loadPriceAlertsFromConfig, ct.loadPortfolioFromConfig, } @@ -295,6 +297,7 @@ func (ct *Cointop) ConfigToToml() ([]byte, error) { Chart: chartMapIfc, CompactNotation: ct.State.compactNotation, EnableMouse: ct.State.enableMouse, + AltCoinLinkCode: ct.State.altCoinLinkCode, } var b bytes.Buffer @@ -522,6 +525,16 @@ func (ct *Cointop) loadEnableMouseFromConfig() error { return nil } +// loadCompactNotationFromConfig loads compact-notation setting from config file to struct +func (ct *Cointop) loadAltCoinLinkCodeFromConfig() error { + log.Debug("loadAltCoinLinkCodeFromConfig()") + if altCoinLinkCode, ok := ct.config.AltCoinLinkCode.(string); ok { + ct.State.altCoinLinkCode = altCoinLinkCode + } + + return nil +} + // LoadAPIChoiceFromConfig loads API choices from config file to struct func (ct *Cointop) loadAPIChoiceFromConfig() error { log.Debug("loadAPIKeysFromConfig()") diff --git a/cointop/default_shortcuts.go b/cointop/default_shortcuts.go index e18988d..5c7cc21 100644 --- a/cointop/default_shortcuts.go +++ b/cointop/default_shortcuts.go @@ -20,6 +20,7 @@ func DefaultShortcuts() map[string]string { "ctrl+d": "page_down", "ctrl+f": "open_search", "ctrl+n": "next_page", + "ctrl+o": "open_alt_link", "ctrl+p": "previous_page", "ctrl+r": "refresh", "ctrl+R": "refresh", diff --git a/cointop/dominance.go b/cointop/dominance.go index a76b247..7d8964f 100644 --- a/cointop/dominance.go +++ b/cointop/dominance.go @@ -20,9 +20,9 @@ func PrintBitcoinDominance(config *DominanceConfig) error { var coinAPI api.Interface if config.APIChoice == CoinMarketCap { - coinAPI = api.NewCMC("") + coinAPI = api.NewCMC("", DefaultAltCoinLinkCode) } else if config.APIChoice == CoinGecko { - coinAPI = api.NewCG(0, 0) + coinAPI = api.NewCG(0, 0, DefaultAltCoinLinkCode) } else { return ErrInvalidAPIChoice } diff --git a/cointop/keybindings.go b/cointop/keybindings.go index eb1af05..02de0ca 100644 --- a/cointop/keybindings.go +++ b/cointop/keybindings.go @@ -130,6 +130,8 @@ func (ct *Cointop) SetKeybindingAction(shortcutKey string, action string) error fn = ct.Keyfn(ct.NavigateLastLine) case "open_link": fn = ct.Keyfn(ct.OpenLink) + case "open_alt_link": + fn = ct.Keyfn(ct.OpenAltLink) case "refresh": fn = ct.Keyfn(ct.Refresh) case "sort_column_asc": diff --git a/cointop/price.go b/cointop/price.go index 203afdc..a65b8e3 100644 --- a/cointop/price.go +++ b/cointop/price.go @@ -55,9 +55,9 @@ func GetCoinPrices(config *PricesConfig) ([]string, error) { } var priceAPI api.Interface if config.APIChoice == CoinMarketCap { - priceAPI = api.NewCMC("") + priceAPI = api.NewCMC("", DefaultAltCoinLinkCode) } else if config.APIChoice == CoinGecko { - priceAPI = api.NewCG(0, 0) + priceAPI = api.NewCG(0, 0, DefaultAltCoinLinkCode) } else { return nil, ErrInvalidAPIChoice } diff --git a/cointop/table.go b/cointop/table.go index b8477d6..162e4db 100644 --- a/cointop/table.go +++ b/cointop/table.go @@ -2,6 +2,7 @@ package cointop import ( "fmt" + apitypes "github.com/cointop-sh/cointop/pkg/api/types" "net/url" "strings" @@ -209,6 +210,25 @@ func (ct *Cointop) RowLink() string { return ct.api.CoinLink(coin.Slug) } +// RowLink returns the row url link +func (ct *Cointop) RowAltLink() string { + log.Debug("RowAltLink()") + coin := ct.HighlightedRowCoin() + if coin == nil { + return "" + } + + apiCoin := apitypes.Coin{ + ID: coin.ID, + Name: coin.Name, + Symbol: coin.Symbol, + Rank: coin.Rank, + Slug: coin.Slug, + } + + return ct.api.AltCoinLink(apiCoin) +} + // RowLinkShort returns a shortened version of the row url link func (ct *Cointop) RowLinkShort() string { log.Debug("RowLinkShort()") diff --git a/cointop/util.go b/cointop/util.go index 6d2b96b..905a826 100644 --- a/cointop/util.go +++ b/cointop/util.go @@ -19,6 +19,13 @@ func (ct *Cointop) OpenLink() error { return nil } +// OpenLink opens the alternate url in a browser +func (ct *Cointop) OpenAltLink() error { + log.Debug("OpenAltLink()") + open.URL(ct.RowAltLink()) + return nil +} + // GetBytes returns the interface in bytes form func GetBytes(key interface{}) ([]byte, error) { var buf bytes.Buffer diff --git a/pkg/api/api.go b/pkg/api/api.go index 29cd450..225cefe 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -6,8 +6,8 @@ import ( ) // NewCMC new CoinMarketCap API -func NewCMC(apiKey string) Interface { - return cmc.NewCMC(apiKey) +func NewCMC(apiKey string, altCoinLinkCode string) Interface { + return cmc.NewCMC(apiKey, altCoinLinkCode) } // NewCC new CryptoCompare API @@ -16,9 +16,10 @@ func NewCC() { } // NewCG new CoinGecko API -func NewCG(perPage, maxPages uint) Interface { +func NewCG(perPage, maxPages uint, altCoinLinkCode string) Interface { return cg.NewCoinGecko(&cg.Config{ - PerPage: perPage, - MaxPages: maxPages, + PerPage: perPage, + MaxPages: maxPages, + AltCoinLinkCode: altCoinLinkCode, }) } diff --git a/pkg/api/impl/coingecko/coingecko.go b/pkg/api/impl/coingecko/coingecko.go index 049d062..7343e54 100644 --- a/pkg/api/impl/coingecko/coingecko.go +++ b/pkg/api/impl/coingecko/coingecko.go @@ -14,6 +14,8 @@ import ( gecko "github.com/cointop-sh/cointop/pkg/api/vendors/coingecko/v3" "github.com/cointop-sh/cointop/pkg/api/vendors/coingecko/v3/types" geckoTypes "github.com/cointop-sh/cointop/pkg/api/vendors/coingecko/v3/types" + "github.com/cointop-sh/cointop/pkg/eval" + log "github.com/sirupsen/logrus" ) // ErrPingFailed is the error for when pinging the API fails @@ -24,8 +26,9 @@ var ErrNotFound = errors.New("not found") // Config config type Config struct { - PerPage uint - MaxPages uint + PerPage uint + MaxPages uint + AltCoinLinkCode string } // Service service @@ -33,6 +36,7 @@ type Service struct { client *gecko.Client maxResultsPerPage uint maxPages uint + altCoinLinkCode string cacheMap sync.Map cachedRates *types.ExchangeRatesItem } @@ -57,6 +61,7 @@ func NewCoinGecko(config *Config) *Service { client: client, maxResultsPerPage: uint(math.Min(float64(maxResults), float64(maxResultsPerPage))), maxPages: maxPages, + altCoinLinkCode: config.AltCoinLinkCode, cacheMap: sync.Map{}, } svc.cacheCoinsIDList() @@ -272,6 +277,23 @@ func (s *Service) CoinLink(slug string) string { return fmt.Sprintf("https://www.coingecko.com/en/coins/%s", slug) } +func (s *Service) AltCoinLink(coin apitypes.Coin) string { + if s.altCoinLinkCode == "" { + return s.CoinLink(coin.Slug) + } + + env := map[string]interface{}{ + "sprintf": fmt.Sprintf, + "coin": coin, + } + if s, err := eval.EvaluateExpressionToString(s.altCoinLinkCode, env); err == nil { + return s + } else { + log.Warnf("Error evaluating AltCoinLink: %v", err) + return "" + } +} + // SupportedCurrencies returns a list of supported currencies func (s *Service) SupportedCurrencies() []string { // keep these in alphabetical order diff --git a/pkg/api/impl/coinmarketcap/coinmarketcap.go b/pkg/api/impl/coinmarketcap/coinmarketcap.go index 0aa28bd..73350c4 100644 --- a/pkg/api/impl/coinmarketcap/coinmarketcap.go +++ b/pkg/api/impl/coinmarketcap/coinmarketcap.go @@ -4,6 +4,8 @@ import ( "encoding/json" "errors" "fmt" + "github.com/cointop-sh/cointop/pkg/eval" + log "github.com/sirupsen/logrus" "io/ioutil" "net/http" "os" @@ -28,11 +30,12 @@ var ErrFetchGraphData = errors.New("graph data fetch error") // Service service type Service struct { - client *cmc.Client + client *cmc.Client + altCoinLinkCode string } // NewCMC new service -func NewCMC(apiKey string) *Service { +func NewCMC(apiKey string, altCoinLinkCode string) *Service { if apiKey == "" { apiKey = os.Getenv("CMC_PRO_API_KEY") } @@ -40,7 +43,8 @@ func NewCMC(apiKey string) *Service { ProAPIKey: apiKey, }) return &Service{ - client: client, + client: client, + altCoinLinkCode: altCoinLinkCode, } } @@ -337,6 +341,24 @@ func (s *Service) CoinLink(slug string) string { return fmt.Sprintf("https://coinmarketcap.com/currencies/%s/", slug) } +func (s *Service) AltCoinLink(coin apitypes.Coin) string { + if s.altCoinLinkCode == "" { + return s.CoinLink(coin.Slug) + } + + // code := `sprintf("https://www.coingecko.com/en/coins/%s", coin.Slug)` + env := map[string]interface{}{ + "sprintf": fmt.Sprintf, + "coin": coin, + } + if s, err := eval.EvaluateExpressionToString(s.altCoinLinkCode, env); err == nil { + return s + } else { + log.Warnf("Error evaluating AltCoinLink: %v", err) + return "" + } +} + // SupportedCurrencies returns a list of supported currencies func (s *Service) SupportedCurrencies() []string { // keep these in alphabetical order diff --git a/pkg/api/interface.go b/pkg/api/interface.go index 206b549..9b2e3f3 100644 --- a/pkg/api/interface.go +++ b/pkg/api/interface.go @@ -14,6 +14,7 @@ type Interface interface { GetCoinData(name string, convert string) (types.Coin, error) GetCoinDataBatch(names []string, convert string) ([]types.Coin, error) CoinLink(slug string) string + AltCoinLink(coin types.Coin) string SupportedCurrencies() []string Price(name string, convert string) (float64, error) GetExchangeRate(convertFrom, convertTo string, cached bool) (float64, error) // I don't love this caching diff --git a/pkg/eval/eval.go b/pkg/eval/eval.go index be9f205..810cece 100644 --- a/pkg/eval/eval.go +++ b/pkg/eval/eval.go @@ -43,3 +43,23 @@ func EvaluateExpressionToFloat64(input string, env interface{}) (float64, error) } return f64, nil } + +func EvaluateExpressionToString(input string, env interface{}) (string, error) { + input = strings.TrimSpace(input) + if input == "" { + return "", nil + } + program, err := expr.Compile(input, expr.Env(env)) + if err != nil { + return "", err + } + result, err := expr.Run(program, env) + if err != nil { + return "", err + } + s, ok := result.(string) + if !ok { + return "", errors.New("expression did not return string type") + } + return s, nil +} From e05acbf95ce9942c5a43e5056f4ecefd0747db1b Mon Sep 17 00:00:00 2001 From: Simon Roberts Date: Thu, 18 Nov 2021 14:46:41 +1100 Subject: [PATCH 2/3] Apply strings.ToUpper() to coin.Symbol when creating alt-link --- cointop/cointop.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cointop/cointop.go b/cointop/cointop.go index 57ca795..f1ad7a6 100644 --- a/cointop/cointop.go +++ b/cointop/cointop.go @@ -197,7 +197,7 @@ var DefaultCompactNotation = false var DefaultEnableMouse = true // DefaultAltCoinLinkCode -var DefaultAltCoinLinkCode = "sprintf(\"https://www.tradingview.com/chart/?symbol=%sUSDT\", coin.Symbol)" +var DefaultAltCoinLinkCode = "sprintf(\"https://www.tradingview.com/chart/?symbol=%sUSDT\", strings.ToUpper(coin.Symbol))" // DefaultMaxChartWidth ... var DefaultMaxChartWidth = 175 From 734345fe5aa4d315407cf10ec1d609bd52503106 Mon Sep 17 00:00:00 2001 From: Simon Roberts Date: Fri, 19 Nov 2021 11:31:17 +1100 Subject: [PATCH 3/3] Fix comment --- cointop/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cointop/config.go b/cointop/config.go index a156eb8..b63a4b5 100644 --- a/cointop/config.go +++ b/cointop/config.go @@ -525,7 +525,7 @@ func (ct *Cointop) loadEnableMouseFromConfig() error { return nil } -// loadCompactNotationFromConfig loads compact-notation setting from config file to struct +// loadAltCoinLinkCodeFromConfig loads AltCoinLinkCode setting from config file to struct func (ct *Cointop) loadAltCoinLinkCodeFromConfig() error { log.Debug("loadAltCoinLinkCodeFromConfig()") if altCoinLinkCode, ok := ct.config.AltCoinLinkCode.(string); ok {