currency conversion

pull/15/head
Miguel Mota 6 years ago
parent 40f0b3a3b3
commit e0b3d59d0c

@ -43,6 +43,7 @@ In action
- Charts for coins and global market graphs - Charts for coins and global market graphs
- Quick chart date range change - Quick chart date range change
- Fuzzy searching for finding coins - Fuzzy searching for finding coins
- Currency conversion
- Save and view favorite coins - Save and view favorite coins
- Color support - Color support
- Help menu - Help menu
@ -163,7 +164,7 @@ Key|Action
<kbd>2</kbd>|Sort table by *[2]4 hour change* <kbd>2</kbd>|Sort table by *[2]4 hour change*
<kbd>7</kbd>|Sort table by *[7] day change* <kbd>7</kbd>|Sort table by *[7] day change*
<kbd>a</kbd>|Sort table by *[a]vailable supply* <kbd>a</kbd>|Sort table by *[a]vailable supply*
<kbd>c</kbd>|Toggle [c]hart for highlighted coin <kbd>c</kbd>|Show currency convert menu
<kbd>f</kbd>|Toggle show favorites <kbd>f</kbd>|Toggle show favorites
<kbd>F</kbd>|Toggle show favorites <kbd>F</kbd>|Toggle show favorites
<kbd>g</kbd>|Go to first line of page (vim inspired) <kbd>g</kbd>|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" left = "previous_page"
right = "next_page" right = "next_page"
up = "move_up" up = "move_up"
c = "toggle_row_chart" c = "show_currency_convert_menu"
"ctrl+c" = "quit" "ctrl+c" = "quit"
"ctrl+d" = "page_down" "ctrl+d" = "page_down"
"ctrl+f" = "open_search" "ctrl+f" = "open_search"
@ -276,6 +277,7 @@ Action|Description
`first_chart_range`|Select first chart date range (e.g. 1H) `first_chart_range`|Select first chart date range (e.g. 1H)
`first_page`|Go to first page `first_page`|Go to first page
`help`|Show help `help`|Show help
`hide_currency_convert_menu`|Hide currency convert menu
`last_chart_range`|Select last chart date range (e.g. All Time) `last_chart_range`|Select last chart date range (e.g. All Time)
`last_page`|Go to last page `last_page`|Go to last page
`move_to_page_first_row`|Move to first row on page `move_to_page_first_row`|Move to first row on page
@ -296,6 +298,8 @@ Action|Description
`quit`|Quit application `quit`|Quit application
`refresh`|Do a manual refresh on the data `refresh`|Do a manual refresh on the data
`save`|Save config `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_1h_change`|Sort table by column *1 hour change*
`sort_column_24h_change`|Sort table by column *24 hour change* `sort_column_24h_change`|Sort table by column *24 hour change*
`sort_column_24h_volume`|Sort table by column *24 hour volume* `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 `sort_right_column`|Sort the column to the right of the highlighted column
`toggle_row_chart`|Toggle the chart for the highlighted row `toggle_row_chart`|Toggle the chart for the highlighted row
`toggle_favorite`|Toggle coin as favorite `toggle_favorite`|Toggle coin as favorite
`toggle_show_currency_convert_menu`|Toggle show currency convert menu
`toggle_show_favorites`|Toggle show favorites `toggle_show_favorites`|Toggle show favorites
## FAQ ## FAQ
@ -422,6 +427,14 @@ Action|Description
<sup><sub>YTD = Year-to-date<sub></sup> <sup><sub>YTD = Year-to-date<sub></sup>
- Q: How do I change the fiat currency?
- A: Press <kbd>c</kbd> 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 ## Development
### Go ### Go

@ -2,49 +2,52 @@ package cointop
func actionsMap() map[string]bool { func actionsMap() map[string]bool {
return map[string]bool{ return map[string]bool{
"first_page": true, "first_page": true,
"help": true, "help": true,
"toggle_show_help": true, "toggle_show_help": true,
"close_help": true, "close_help": true,
"last_page": true, "last_page": true,
"move_to_page_first_row": true, "move_to_page_first_row": true,
"move_to_page_last_row": true, "move_to_page_last_row": true,
"move_to_page_visible_first_row": true, "move_to_page_visible_first_row": true,
"move_to_page_visible_last_row": true, "move_to_page_visible_last_row": true,
"move_to_page_visible_middle_row": true, "move_to_page_visible_middle_row": true,
"move_up": true, "move_up": true,
"move_down": true, "move_down": true,
"next_page": true, "next_page": true,
"open_link": true, "open_link": true,
"page_down": true, "page_down": true,
"page_up": true, "page_up": true,
"previous_page": true, "previous_page": true,
"quit": true, "quit": true,
"refresh": true, "refresh": true,
"sort_column_1h_change": true, "sort_column_1h_change": true,
"sort_column_24h_change": true, "sort_column_24h_change": true,
"sort_column_24h_volume": true, "sort_column_24h_volume": true,
"sort_column_7d_change": true, "sort_column_7d_change": true,
"sort_column_asc": true, "sort_column_asc": true,
"sort_column_available_supply": true, "sort_column_available_supply": true,
"sort_column_desc": true, "sort_column_desc": true,
"sort_column_last_updated": true, "sort_column_last_updated": true,
"sort_column_market_cap": true, "sort_column_market_cap": true,
"sort_column_name": true, "sort_column_name": true,
"sort_column_price": true, "sort_column_price": true,
"sort_column_rank": true, "sort_column_rank": true,
"sort_column_symbol": true, "sort_column_symbol": true,
"sort_column_total_supply": true, "sort_column_total_supply": true,
"sort_left_column": true, "sort_left_column": true,
"sort_right_column": true, "sort_right_column": true,
"toggle_row_chart": true, "toggle_row_chart": true,
"open_search": true, "open_search": true,
"toggle_favorite": true, "toggle_favorite": true,
"toggle_show_favorites": true, "toggle_show_favorites": true,
"previous_chart_range": true, "previous_chart_range": true,
"next_chart_range": true, "next_chart_range": true,
"first_chart_range": true, "first_chart_range": true,
"last_chart_range": true, "last_chart_range": true,
"toggle_show_currency_convert_menu": true,
"show_currency_convert_menu": true,
"hide_currency_convert_menu": true,
} }
} }

@ -64,6 +64,9 @@ type Cointop struct {
helpviewname string helpviewname string
helpvisible bool helpvisible bool
currencyconversion string currencyconversion string
convertmenuview *gocui.View
convertmenuviewname string
convertmenuvisible bool
} }
// Instance running cointop instance // Instance running cointop instance
@ -136,6 +139,7 @@ func Run() {
statusbarviewname: "statusbar", statusbarviewname: "statusbar",
searchfieldviewname: "searchfield", searchfieldviewname: "searchfield",
helpviewname: "help", helpviewname: "help",
convertmenuviewname: "convertmenu",
currencyconversion: "USD", currencyconversion: "USD",
} }
Instance = &ct Instance = &ct
@ -180,7 +184,7 @@ func Run() {
} }
func (ct *Cointop) quit() error { func (ct *Cointop) quit() error {
if ct.helpvisible || ct.searchfieldvisible { if ct.helpvisible || ct.convertmenuvisible || ct.searchfieldvisible {
return nil return nil
} }

@ -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
}
}

@ -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, " "))) header := color.GreenBg(fmt.Sprintf(" Help %s\n\n", pad.Left("[q] close help ", ct.maxtablewidth-10, " ")))
cnt := 0 cnt := 0
h := ct.viewHeight("help") h := ct.viewHeight(ct.helpviewname)
percol := h - 3 percol := h - 3
cols := make([][]string, percol) cols := make([][]string, percol)
for i := range cols { for i := range cols {
@ -63,7 +63,7 @@ func (ct *Cointop) updateHelp() {
func (ct *Cointop) showHelp() error { func (ct *Cointop) showHelp() error {
ct.helpvisible = true ct.helpvisible = true
ct.updateHelp() ct.updateHelp()
ct.setActiveView("help") ct.setActiveView(ct.helpviewname)
return nil return nil
} }

@ -241,6 +241,9 @@ func (ct *Cointop) keybindings(g *gocui.Gui) error {
case "toggle_show_help": case "toggle_show_help":
fn = ct.keyfn(ct.toggleHelp) fn = ct.keyfn(ct.toggleHelp)
view = "" view = ""
case "show_help":
fn = ct.keyfn(ct.showHelp)
view = ""
case "hide_help": case "hide_help":
fn = ct.keyfn(ct.hideHelp) fn = ct.keyfn(ct.hideHelp)
view = "help" view = "help"
@ -298,6 +301,15 @@ func (ct *Cointop) keybindings(g *gocui.Gui) error {
fn = ct.keyfn(ct.firstChartRange) fn = ct.keyfn(ct.firstChartRange)
case "last_chart_range": case "last_chart_range":
fn = ct.keyfn(ct.lastChartRange) 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: default:
fn = ct.keyfn(ct.noop) 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), "") ct.setKeybindingMod(gocui.KeyCtrlZ, gocui.ModNone, ct.keyfn(ct.forceQuit), "")
// searchfield keys // searchfield keys
ct.setKeybindingMod(gocui.KeyEnter, gocui.ModNone, ct.keyfn(ct.doSearch), "searchfield") ct.setKeybindingMod(gocui.KeyEnter, gocui.ModNone, ct.keyfn(ct.doSearch), ct.searchfieldviewname)
ct.setKeybindingMod(gocui.KeyEsc, gocui.ModNone, ct.keyfn(ct.cancelSearch), "searchfield") ct.setKeybindingMod(gocui.KeyEsc, gocui.ModNone, ct.keyfn(ct.cancelSearch), ct.searchfieldviewname)
// keys to quit help when open // keys to quit help when open
ct.setKeybindingMod(gocui.KeyEsc, 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), "help") ct.setKeybindingMod('q', gocui.ModNone, ct.keyfn(ct.hideHelp), ct.helpviewname)
ct.setKeybindingMod('x', gocui.ModNone, ct.keyfn(ct.hideHelp), "help") ct.setKeybindingMod('x', gocui.ModNone, ct.keyfn(ct.hideHelp), ct.helpviewname)
ct.setKeybindingMod('c', gocui.ModNone, ct.keyfn(ct.hideHelp), "help") 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 return nil
} }

@ -113,12 +113,23 @@ func (ct *Cointop) layout(g *gocui.Gui) error {
ct.helpview.Frame = false ct.helpview.Frame = false
ct.helpview.BgColor = gocui.ColorBlack ct.helpview.BgColor = gocui.ColorBlack
ct.helpview.FgColor = gocui.ColorWhite 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. // run only once on init.
// this bit of code should be at the bottom // this bit of code should be at the bottom
ct.g = g ct.g = g
g.SetViewOnBottom(ct.searchfieldviewname) // hide g.SetViewOnBottom(ct.searchfieldviewname) // hide
g.SetViewOnBottom(ct.helpviewname) // hide g.SetViewOnBottom(ct.helpviewname) // hide
g.SetViewOnBottom(ct.convertmenuviewname) // hide
ct.setActiveView(ct.tableviewname) ct.setActiveView(ct.tableviewname)
ct.intervalFetchData() ct.intervalFetchData()
} }

@ -32,7 +32,8 @@ func defaultShortcuts() map[string]string {
"2": "sort_column_24h_change", "2": "sort_column_24h_change",
"7": "sort_column_7d_change", "7": "sort_column_7d_change",
"a": "sort_column_available_supply", "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",
"F": "toggle_show_favorites", "F": "toggle_show_favorites",
"g": "move_to_page_first_row", "g": "move_to_page_first_row",

@ -11,7 +11,7 @@ func (ct *Cointop) updateStatusbar(s string) {
ct.statusbarview.Clear() ct.statusbarview.Clear()
currpage := ct.currentDisplayPage() currpage := ct.currentDisplayPage()
totalpages := ct.totalPages() 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, " ")) fmt.Fprintln(ct.statusbarview, pad.Right(fmt.Sprintf("%v %sPage %v/%v %s", base, "[← →]", currpage, totalpages, s), ct.maxtablewidth, " "))
}) })
} }

@ -142,6 +142,9 @@ func (ct *Cointop) updateTable() error {
if end < 0 { if end < 0 {
end = 0 end = 0
} }
if start >= end {
return nil
}
if end > 0 { if end > 0 {
sliced = allcoins[start:end] sliced = allcoins[start:end]
} }

@ -2,6 +2,7 @@ package api
import ( import (
"strconv" "strconv"
"sync"
apitypes "github.com/miguelmota/cointop/pkg/api/types" apitypes "github.com/miguelmota/cointop/pkg/api/types"
cmc "github.com/miguelmota/cointop/pkg/cmc" cmc "github.com/miguelmota/cointop/pkg/cmc"
@ -16,11 +17,15 @@ func New() *Service {
return &Service{} return &Service{}
} }
// GetAllCoinData gets all coin data // https://api.coinmarketcap.com/v1/ticker/?start=0&limit=0
func (s *Service) GetAllCoinData(convert string) (map[string]apitypes.Coin, error) {
func getLimitedCoinData(convert string, offset int) (map[string]apitypes.Coin, error) {
ret := make(map[string]apitypes.Coin) ret := make(map[string]apitypes.Coin)
max := 100
coins, err := cmc.Tickers(&cmc.TickersOptions{ coins, err := cmc.Tickers(&cmc.TickersOptions{
Convert: convert, Convert: convert,
Start: max * offset,
Limit: max,
}) })
if err != nil { if err != nil {
return ret, err return ret, err
@ -46,6 +51,27 @@ func (s *Service) GetAllCoinData(convert string) (map[string]apitypes.Coin, erro
return ret, nil 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 // GetCoinGraphData gets coin graph data
func (s *Service) GetCoinGraphData(coin string, start int64, end int64) (apitypes.CoinGraph, error) { func (s *Service) GetCoinGraphData(coin string, start int64, end int64) (apitypes.CoinGraph, error) {
ret := apitypes.CoinGraph{} ret := apitypes.CoinGraph{}

Loading…
Cancel
Save