diff --git a/README.md b/README.md
index 5276e08..926d43d 100644
--- a/README.md
+++ b/README.md
@@ -43,6 +43,7 @@ In action
- Charts for coins and global market graphs
- Quick chart date range change
- Fuzzy searching for finding coins
+- Currency conversion
- Save and view favorite coins
- Color support
- Help menu
@@ -163,7 +164,7 @@ Key|Action
2|Sort table by *[2]4 hour change*
7|Sort table by *[7] day change*
a|Sort table by *[a]vailable supply*
-c|Toggle [c]hart for highlighted coin
+c|Show currency convert menu
f|Toggle show favorites
F|Toggle show favorites
g|Go to first line of page (vim inspired)
@@ -231,7 +232,7 @@ You can then configure the actions you want for each key:
left = "previous_page"
right = "next_page"
up = "move_up"
- c = "toggle_row_chart"
+ c = "show_currency_convert_menu"
"ctrl+c" = "quit"
"ctrl+d" = "page_down"
"ctrl+f" = "open_search"
@@ -276,6 +277,7 @@ Action|Description
`first_chart_range`|Select first chart date range (e.g. 1H)
`first_page`|Go to first page
`help`|Show help
+`hide_currency_convert_menu`|Hide currency convert menu
`last_chart_range`|Select last chart date range (e.g. All Time)
`last_page`|Go to last page
`move_to_page_first_row`|Move to first row on page
@@ -296,6 +298,8 @@ Action|Description
`quit`|Quit application
`refresh`|Do a manual refresh on the data
`save`|Save config
+`show_currency_convert_menu`|Show currency convert menu
+`show_favorites`|Show favorites
`sort_column_1h_change`|Sort table by column *1 hour change*
`sort_column_24h_change`|Sort table by column *24 hour change*
`sort_column_24h_volume`|Sort table by column *24 hour volume*
@@ -314,6 +318,7 @@ Action|Description
`sort_right_column`|Sort the column to the right of the highlighted column
`toggle_row_chart`|Toggle the chart for the highlighted row
`toggle_favorite`|Toggle coin as favorite
+`toggle_show_currency_convert_menu`|Toggle show currency convert menu
`toggle_show_favorites`|Toggle show favorites
## FAQ
@@ -422,6 +427,14 @@ Action|Description
YTD = Year-to-date
+- Q: How do I change the fiat currency?
+
+ - A: Press c to show the currency convert menu, and press the corresponding key to select that as the fiat currency.
+
+- Q: Which currencies can I convert to?
+
+ - A: The supported fiat currencies for conversion are `USD`, `EUR`, `GBP`, `CNY`, `HKD`, `JPY`, `KRW`, `NZD`, `CFH`, `MXN`, `AUD`, `IDR`, `RUB`, and `CAD`. The supported crypto currencies for conversion are `BTC` and `ETH`.
+
## Development
### Go
diff --git a/cointop/actions.go b/cointop/actions.go
index 993bed5..fab59a9 100644
--- a/cointop/actions.go
+++ b/cointop/actions.go
@@ -2,49 +2,52 @@ package cointop
func actionsMap() map[string]bool {
return map[string]bool{
- "first_page": true,
- "help": true,
- "toggle_show_help": true,
- "close_help": true,
- "last_page": true,
- "move_to_page_first_row": true,
- "move_to_page_last_row": true,
- "move_to_page_visible_first_row": true,
- "move_to_page_visible_last_row": true,
- "move_to_page_visible_middle_row": true,
- "move_up": true,
- "move_down": true,
- "next_page": true,
- "open_link": true,
- "page_down": true,
- "page_up": true,
- "previous_page": true,
- "quit": true,
- "refresh": true,
- "sort_column_1h_change": true,
- "sort_column_24h_change": true,
- "sort_column_24h_volume": true,
- "sort_column_7d_change": true,
- "sort_column_asc": true,
- "sort_column_available_supply": true,
- "sort_column_desc": true,
- "sort_column_last_updated": true,
- "sort_column_market_cap": true,
- "sort_column_name": true,
- "sort_column_price": true,
- "sort_column_rank": true,
- "sort_column_symbol": true,
- "sort_column_total_supply": true,
- "sort_left_column": true,
- "sort_right_column": true,
- "toggle_row_chart": true,
- "open_search": true,
- "toggle_favorite": true,
- "toggle_show_favorites": true,
- "previous_chart_range": true,
- "next_chart_range": true,
- "first_chart_range": true,
- "last_chart_range": true,
+ "first_page": true,
+ "help": true,
+ "toggle_show_help": true,
+ "close_help": true,
+ "last_page": true,
+ "move_to_page_first_row": true,
+ "move_to_page_last_row": true,
+ "move_to_page_visible_first_row": true,
+ "move_to_page_visible_last_row": true,
+ "move_to_page_visible_middle_row": true,
+ "move_up": true,
+ "move_down": true,
+ "next_page": true,
+ "open_link": true,
+ "page_down": true,
+ "page_up": true,
+ "previous_page": true,
+ "quit": true,
+ "refresh": true,
+ "sort_column_1h_change": true,
+ "sort_column_24h_change": true,
+ "sort_column_24h_volume": true,
+ "sort_column_7d_change": true,
+ "sort_column_asc": true,
+ "sort_column_available_supply": true,
+ "sort_column_desc": true,
+ "sort_column_last_updated": true,
+ "sort_column_market_cap": true,
+ "sort_column_name": true,
+ "sort_column_price": true,
+ "sort_column_rank": true,
+ "sort_column_symbol": true,
+ "sort_column_total_supply": true,
+ "sort_left_column": true,
+ "sort_right_column": true,
+ "toggle_row_chart": true,
+ "open_search": true,
+ "toggle_favorite": true,
+ "toggle_show_favorites": true,
+ "previous_chart_range": true,
+ "next_chart_range": true,
+ "first_chart_range": true,
+ "last_chart_range": true,
+ "toggle_show_currency_convert_menu": true,
+ "show_currency_convert_menu": true,
+ "hide_currency_convert_menu": true,
}
}
diff --git a/cointop/cointop.go b/cointop/cointop.go
index 2c37445..c085a28 100644
--- a/cointop/cointop.go
+++ b/cointop/cointop.go
@@ -64,6 +64,9 @@ type Cointop struct {
helpviewname string
helpvisible bool
currencyconversion string
+ convertmenuview *gocui.View
+ convertmenuviewname string
+ convertmenuvisible bool
}
// Instance running cointop instance
@@ -136,6 +139,7 @@ func Run() {
statusbarviewname: "statusbar",
searchfieldviewname: "searchfield",
helpviewname: "help",
+ convertmenuviewname: "convertmenu",
currencyconversion: "USD",
}
Instance = &ct
@@ -180,7 +184,7 @@ func Run() {
}
func (ct *Cointop) quit() error {
- if ct.helpvisible || ct.searchfieldvisible {
+ if ct.helpvisible || ct.convertmenuvisible || ct.searchfieldvisible {
return nil
}
diff --git a/cointop/conversions.go b/cointop/conversions.go
new file mode 100644
index 0000000..d7b39ab
--- /dev/null
+++ b/cointop/conversions.go
@@ -0,0 +1,141 @@
+package cointop
+
+import (
+ "fmt"
+ "sort"
+
+ "github.com/miguelmota/cointop/pkg/color"
+ "github.com/miguelmota/cointop/pkg/pad"
+)
+
+//var supportedfiatconversions = []string{"AUD", "BRL", "CAD", "CHF", "CLP", "CNY", "CZK", "DKK", "EUR", "GBP", "HKD", "HUF", "IDR", "ILS", "INR", "JPY", "KRW", "MXN", "MYR", "NOK", "NZD", "PHP", "PKR", "PLN", "RUB", "SEK", "SGD", "THB", "TRY", "TWD", "ZAR"}
+
+var supportedfiatconversions = map[string]string{
+ "USD": "US Dollar",
+ "EUR": "Euro",
+ "GBP": "British Pound",
+ "CNY": "Chinese Yuan",
+ "HKD": "Hong Kong Dollar",
+ "JPY": "Japanese Yen",
+ "KRW": "South Korean Won",
+ "NZD": "New Zealand Dollar",
+ "CFH": "Swiss Franc",
+ "MXN": "Mexican Peso",
+ "AUD": "Australian Dollar",
+ "IDR": "Indonesian Rupiah",
+ "RUB": "Russian Ruble",
+ "CAD": "Canadian dollar",
+}
+
+var supportedcryptoconversion = map[string]string{
+ "BTC": "Bitcoin",
+ "ETH": "Ethereum",
+}
+
+var alphanumericcharacters = []rune{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'}
+
+func (ct *Cointop) supportedCurrencyConversions() map[string]string {
+ all := map[string]string{}
+ for k, v := range supportedfiatconversions {
+ all[k] = v
+ }
+ for k, v := range supportedcryptoconversion {
+ all[k] = v
+ }
+ return all
+}
+
+func (ct *Cointop) supportedFiatCurrencyConversions() map[string]string {
+ return supportedfiatconversions
+}
+
+func (ct *Cointop) supportedCryptoCurrencyConversions() map[string]string {
+ return supportedfiatconversions
+}
+
+func (ct *Cointop) sortedSupportedCurrencyConversions() []string {
+ currencies := ct.supportedCurrencyConversions()
+ keys := make([]string, 0, len(currencies))
+ for k := range currencies {
+ keys = append(keys, k)
+ }
+ sort.Strings(keys)
+ return keys
+}
+
+func (ct *Cointop) toggleConvertMenu() error {
+ ct.convertmenuvisible = !ct.convertmenuvisible
+ if ct.convertmenuvisible {
+ return ct.showConvertMenu()
+ }
+ return ct.hideConvertMenu()
+}
+
+func (ct *Cointop) updateConvertMenu() {
+ header := color.GreenBg(fmt.Sprintf(" Currency Conversion %s\n\n", pad.Left("[q] close menu ", ct.maxtablewidth-20, " ")))
+ helpline := " Press the corresponding key to select currency for conversion\n\n"
+ cnt := 0
+ h := ct.viewHeight(ct.convertmenuviewname)
+ percol := h - 5
+ cols := make([][]string, percol)
+ for i := range cols {
+ cols[i] = make([]string, 20)
+ }
+
+ keys := ct.sortedSupportedCurrencyConversions()
+ currencies := ct.supportedCurrencyConversions()
+ for i, key := range keys {
+ currency := currencies[key]
+ if cnt%percol == 0 {
+ cnt = 0
+ }
+ item := fmt.Sprintf(" [ %1s ] %4s %-34s", string(alphanumericcharacters[i]), key, color.Yellow(currency))
+ cols[cnt] = append(cols[cnt], item)
+ cnt = cnt + 1
+ }
+
+ var body string
+ for i := 0; i < percol; i++ {
+ var row string
+ for j := 0; j < len(cols[i]); j++ {
+ item := cols[i][j]
+ row = fmt.Sprintf("%s%s", row, item)
+ }
+ body = fmt.Sprintf("%s%s\n", body, row)
+ }
+
+ content := fmt.Sprintf("%s%s%s", header, helpline, body)
+ ct.update(func() {
+ ct.convertmenuview.Clear()
+ ct.convertmenuview.Frame = true
+ fmt.Fprintln(ct.convertmenuview, content)
+ })
+}
+
+func (ct *Cointop) showConvertMenu() error {
+ ct.convertmenuvisible = true
+ ct.updateConvertMenu()
+ ct.setActiveView(ct.convertmenuviewname)
+ return nil
+}
+
+func (ct *Cointop) hideConvertMenu() error {
+ ct.convertmenuvisible = false
+ ct.setViewOnBottom(ct.convertmenuviewname)
+ ct.setActiveView(ct.tableviewname)
+ ct.update(func() {
+ ct.convertmenuview.Clear()
+ ct.convertmenuview.Frame = false
+ fmt.Fprintln(ct.convertmenuview, "")
+ })
+ return nil
+}
+
+func (ct *Cointop) setCurrencyConverstion(convert string) func() error {
+ return func() error {
+ ct.currencyconversion = convert
+ ct.hideConvertMenu()
+ go ct.refreshAll()
+ return nil
+ }
+}
diff --git a/cointop/help.go b/cointop/help.go
index c69b180..e8bf51a 100644
--- a/cointop/help.go
+++ b/cointop/help.go
@@ -25,7 +25,7 @@ func (ct *Cointop) updateHelp() {
header := color.GreenBg(fmt.Sprintf(" Help %s\n\n", pad.Left("[q] close help ", ct.maxtablewidth-10, " ")))
cnt := 0
- h := ct.viewHeight("help")
+ h := ct.viewHeight(ct.helpviewname)
percol := h - 3
cols := make([][]string, percol)
for i := range cols {
@@ -63,7 +63,7 @@ func (ct *Cointop) updateHelp() {
func (ct *Cointop) showHelp() error {
ct.helpvisible = true
ct.updateHelp()
- ct.setActiveView("help")
+ ct.setActiveView(ct.helpviewname)
return nil
}
diff --git a/cointop/keybindings.go b/cointop/keybindings.go
index d723da3..c9acc36 100644
--- a/cointop/keybindings.go
+++ b/cointop/keybindings.go
@@ -241,6 +241,9 @@ func (ct *Cointop) keybindings(g *gocui.Gui) error {
case "toggle_show_help":
fn = ct.keyfn(ct.toggleHelp)
view = ""
+ case "show_help":
+ fn = ct.keyfn(ct.showHelp)
+ view = ""
case "hide_help":
fn = ct.keyfn(ct.hideHelp)
view = "help"
@@ -298,6 +301,15 @@ func (ct *Cointop) keybindings(g *gocui.Gui) error {
fn = ct.keyfn(ct.firstChartRange)
case "last_chart_range":
fn = ct.keyfn(ct.lastChartRange)
+ case "toggle_show_currency_convert_menu":
+ fn = ct.keyfn(ct.toggleConvertMenu)
+ view = ""
+ case "show_currency_convert_menu":
+ fn = ct.keyfn(ct.showConvertMenu)
+ view = ""
+ case "hide_currency_convert_menu":
+ fn = ct.keyfn(ct.hideConvertMenu)
+ view = "convertmenu"
default:
fn = ct.keyfn(ct.noop)
}
@@ -310,14 +322,27 @@ func (ct *Cointop) keybindings(g *gocui.Gui) error {
ct.setKeybindingMod(gocui.KeyCtrlZ, gocui.ModNone, ct.keyfn(ct.forceQuit), "")
// searchfield keys
- ct.setKeybindingMod(gocui.KeyEnter, gocui.ModNone, ct.keyfn(ct.doSearch), "searchfield")
- ct.setKeybindingMod(gocui.KeyEsc, gocui.ModNone, ct.keyfn(ct.cancelSearch), "searchfield")
+ ct.setKeybindingMod(gocui.KeyEnter, gocui.ModNone, ct.keyfn(ct.doSearch), ct.searchfieldviewname)
+ ct.setKeybindingMod(gocui.KeyEsc, gocui.ModNone, ct.keyfn(ct.cancelSearch), ct.searchfieldviewname)
// keys to quit help when open
- ct.setKeybindingMod(gocui.KeyEsc, gocui.ModNone, ct.keyfn(ct.hideHelp), "help")
- ct.setKeybindingMod('q', gocui.ModNone, ct.keyfn(ct.hideHelp), "help")
- ct.setKeybindingMod('x', gocui.ModNone, ct.keyfn(ct.hideHelp), "help")
- ct.setKeybindingMod('c', gocui.ModNone, ct.keyfn(ct.hideHelp), "help")
+ ct.setKeybindingMod(gocui.KeyEsc, gocui.ModNone, ct.keyfn(ct.hideHelp), ct.helpviewname)
+ ct.setKeybindingMod('q', gocui.ModNone, ct.keyfn(ct.hideHelp), ct.helpviewname)
+ ct.setKeybindingMod('x', gocui.ModNone, ct.keyfn(ct.hideHelp), ct.helpviewname)
+ ct.setKeybindingMod('c', gocui.ModNone, ct.keyfn(ct.hideHelp), ct.helpviewname)
+
+ // keys to quit convert menu when open
+ ct.setKeybindingMod(gocui.KeyEsc, gocui.ModNone, ct.keyfn(ct.hideConvertMenu), ct.convertmenuviewname)
+ ct.setKeybindingMod('q', gocui.ModNone, ct.keyfn(ct.hideConvertMenu), ct.convertmenuviewname)
+ ct.setKeybindingMod('x', gocui.ModNone, ct.keyfn(ct.hideConvertMenu), ct.convertmenuviewname)
+
+ // character key press to select option
+ // TODO: use scrolling table
+ keys := ct.sortedSupportedCurrencyConversions()
+ for i, k := range keys {
+ ct.setKeybindingMod(rune(alphanumericcharacters[i]), gocui.ModNone, ct.keyfn(ct.setCurrencyConverstion(k)), ct.convertmenuviewname)
+ }
+
return nil
}
diff --git a/cointop/layout.go b/cointop/layout.go
index 5b3e316..d37afbd 100644
--- a/cointop/layout.go
+++ b/cointop/layout.go
@@ -113,12 +113,23 @@ func (ct *Cointop) layout(g *gocui.Gui) error {
ct.helpview.Frame = false
ct.helpview.BgColor = gocui.ColorBlack
ct.helpview.FgColor = gocui.ColorWhite
+ }
+
+ if v, err := g.SetView(ct.convertmenuviewname, 1, 1, ct.maxtablewidth-2, maxY-1); err != nil {
+ if err != gocui.ErrUnknownView {
+ return err
+ }
+ ct.convertmenuview = v
+ ct.convertmenuview.Frame = false
+ ct.convertmenuview.BgColor = gocui.ColorBlack
+ ct.convertmenuview.FgColor = gocui.ColorWhite
// run only once on init.
// this bit of code should be at the bottom
ct.g = g
g.SetViewOnBottom(ct.searchfieldviewname) // hide
g.SetViewOnBottom(ct.helpviewname) // hide
+ g.SetViewOnBottom(ct.convertmenuviewname) // hide
ct.setActiveView(ct.tableviewname)
ct.intervalFetchData()
}
diff --git a/cointop/shortcuts.go b/cointop/shortcuts.go
index 9ad6506..7993c12 100644
--- a/cointop/shortcuts.go
+++ b/cointop/shortcuts.go
@@ -32,7 +32,8 @@ func defaultShortcuts() map[string]string {
"2": "sort_column_24h_change",
"7": "sort_column_7d_change",
"a": "sort_column_available_supply",
- "c": "toggle_row_chart",
+ "c": "show_currency_convert_menu",
+ "C": "show_currency_convert_menu",
"f": "toggle_show_favorites",
"F": "toggle_show_favorites",
"g": "move_to_page_first_row",
diff --git a/cointop/statusbar.go b/cointop/statusbar.go
index 59593b1..ad7a02f 100644
--- a/cointop/statusbar.go
+++ b/cointop/statusbar.go
@@ -11,7 +11,7 @@ func (ct *Cointop) updateStatusbar(s string) {
ct.statusbarview.Clear()
currpage := ct.currentDisplayPage()
totalpages := ct.totalPages()
- base := fmt.Sprintf("%sQuit %sHelp %sChart %sRange %sSearch", "[Q]", "[?]", "[Enter]", "[[ ]]", "[/]")
+ base := fmt.Sprintf("%sQuit %sHelp %sChart %sRange %sSearch %sConvert", "[Q]", "[?]", "[Enter]", "[[ ]]", "[/]", "[C]")
fmt.Fprintln(ct.statusbarview, pad.Right(fmt.Sprintf("%v %sPage %v/%v %s", base, "[← →]", currpage, totalpages, s), ct.maxtablewidth, " "))
})
}
diff --git a/cointop/table.go b/cointop/table.go
index 910b961..840299f 100644
--- a/cointop/table.go
+++ b/cointop/table.go
@@ -142,6 +142,9 @@ func (ct *Cointop) updateTable() error {
if end < 0 {
end = 0
}
+ if start >= end {
+ return nil
+ }
if end > 0 {
sliced = allcoins[start:end]
}
diff --git a/pkg/api/cmc/cmc.go b/pkg/api/cmc/cmc.go
index 66b7574..333ebb1 100644
--- a/pkg/api/cmc/cmc.go
+++ b/pkg/api/cmc/cmc.go
@@ -2,6 +2,7 @@ package api
import (
"strconv"
+ "sync"
apitypes "github.com/miguelmota/cointop/pkg/api/types"
cmc "github.com/miguelmota/cointop/pkg/cmc"
@@ -16,11 +17,15 @@ func New() *Service {
return &Service{}
}
-// GetAllCoinData gets all coin data
-func (s *Service) GetAllCoinData(convert string) (map[string]apitypes.Coin, error) {
+// https://api.coinmarketcap.com/v1/ticker/?start=0&limit=0
+
+func getLimitedCoinData(convert string, offset int) (map[string]apitypes.Coin, error) {
ret := make(map[string]apitypes.Coin)
+ max := 100
coins, err := cmc.Tickers(&cmc.TickersOptions{
Convert: convert,
+ Start: max * offset,
+ Limit: max,
})
if err != nil {
return ret, err
@@ -46,6 +51,27 @@ func (s *Service) GetAllCoinData(convert string) (map[string]apitypes.Coin, erro
return ret, nil
}
+// GetAllCoinData gets all coin data
+func (s *Service) GetAllCoinData(convert string) (map[string]apitypes.Coin, error) {
+ var wg sync.WaitGroup
+ ret := make(map[string]apitypes.Coin)
+ for i := 0; i < 10; i++ {
+ wg.Add(1)
+ go func(j int) {
+ coins, err := getLimitedCoinData(convert, j)
+ if err != nil {
+ return
+ }
+ for k, v := range coins {
+ ret[k] = v
+ }
+ defer wg.Done()
+ }(i)
+ }
+ wg.Wait()
+ return ret, nil
+}
+
// GetCoinGraphData gets coin graph data
func (s *Service) GetCoinGraphData(coin string, start int64, end int64) (apitypes.CoinGraph, error) {
ret := apitypes.CoinGraph{}