pull/42/head
Miguel Mota 5 years ago
parent bedbda8e00
commit 4ef4f7aa54
No known key found for this signature in database
GPG Key ID: 67EC1161588A00F9

@ -644,6 +644,10 @@ Frequently asked questions:
cointop --coinmarketcap-api-key=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx cointop --coinmarketcap-api-key=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
``` ```
- Q: I can I add my own API to cointop?
- A: Fork cointop and add the API that implements the [interface](https://github.com/miguelmota/cointop/blob/master/cointop/common/api/interface.go) to [`cointop/cointop/common/api/impl/`](https://github.com/miguelmota/cointop/tree/master/cointop/common/api/impl). You can use the [CoinGecko implementatoin](https://github.com/miguelmota/cointop/blob/master/cointop/common/api/impl/coingecko/coingecko.go) as reference.
- Q: I installed cointop without errors but the command is not found. - Q: I installed cointop without errors but the command is not found.
- A: Make sure your `GOPATH` and `PATH` is set correctly. - A: Make sure your `GOPATH` and `PATH` is set correctly.
@ -886,7 +890,6 @@ Frequently asked questions:
-A: Cointop uses ~15MB of memory so you can run it on a Raspberry Pi Zero if you wanted to (one reason why cointop was built using Go instead of Node.js or Python). -A: Cointop uses ~15MB of memory so you can run it on a Raspberry Pi Zero if you wanted to (one reason why cointop was built using Go instead of Node.js or Python).
- Q: How does cointop differ from [rate.sx](https://rate.sx/)? - Q: How does cointop differ from [rate.sx](https://rate.sx/)?
- A: *rate.sx* is great for one-off queries or fetching data for bash scripts because it doesn't require installing anything. Cointop differs in that it is interactive and also supports more currencies. - A: *rate.sx* is great for one-off queries or fetching data for bash scripts because it doesn't require installing anything. Cointop differs in that it is interactive and also supports more currencies.

@ -55,5 +55,5 @@ func actionsMap() map[string]bool {
} }
func (ct *Cointop) actionExists(action string) bool { func (ct *Cointop) actionExists(action string) bool {
return ct.actionsmap[action] return ct.actionsMap[action]
} }

@ -11,38 +11,38 @@ import (
"github.com/miguelmota/cointop/cointop/common/timeutil" "github.com/miguelmota/cointop/cointop/common/timeutil"
) )
var chartlock sync.Mutex var chartLock sync.Mutex
var chartpointslock sync.Mutex var chartPointsLock sync.Mutex
func (ct *Cointop) updateChart() error { func (ct *Cointop) updateChart() error {
if ct.chartview == nil { if ct.Views.Chart.Backing == nil {
return nil return nil
} }
chartlock.Lock() chartLock.Lock()
defer chartlock.Unlock() defer chartLock.Unlock()
if ct.portfoliovisible { if ct.State.portfolioVisible {
if err := ct.portfolioChart(); err != nil { if err := ct.portfolioChart(); err != nil {
return err return err
} }
} else { } else {
symbol := ct.selectedCoinSymbol() symbol := ct.selectedCoinSymbol()
name := ct.selectedCoinName() name := ct.selectedCoinName()
ct.chartPoints(symbol, name) ct.calcChartPoints(symbol, name)
} }
if len(ct.chartpoints) != 0 { if len(ct.State.chartPoints) != 0 {
ct.chartview.Clear() ct.Views.Chart.Backing.Clear()
} }
var body string var body string
if len(ct.chartpoints) == 0 { if len(ct.State.chartPoints) == 0 {
body = "\n\n\n\n\nnot enough data for chart" body = "\n\n\n\n\nnot enough data for chart"
} else { } else {
for i := range ct.chartpoints { for i := range ct.State.chartPoints {
var s string var s string
for j := range ct.chartpoints[i] { for j := range ct.State.chartPoints[i] {
p := ct.chartpoints[i][j] p := ct.State.chartPoints[i][j]
s = fmt.Sprintf("%s%c", s, p.Ch) s = fmt.Sprintf("%s%c", s, p.Ch)
} }
body = fmt.Sprintf("%s%s\n", body, s) body = fmt.Sprintf("%s%s\n", body, s)
@ -50,20 +50,20 @@ func (ct *Cointop) updateChart() error {
} }
} }
ct.update(func() { ct.update(func() {
if ct.chartview == nil { if ct.Views.Chart.Backing == nil {
return return
} }
fmt.Fprint(ct.chartview, ct.colorscheme.Chart(body)) fmt.Fprint(ct.Views.Chart.Backing, ct.colorscheme.Chart(body))
}) })
return nil return nil
} }
func (ct *Cointop) chartPoints(symbol string, name string) error { func (ct *Cointop) calcChartPoints(symbol string, name string) error {
maxX := ct.maxtablewidth - 3 maxX := ct.maxTableWidth - 3
chartpointslock.Lock() chartPointsLock.Lock()
defer chartpointslock.Unlock() defer chartPointsLock.Unlock()
// TODO: not do this (SoC) // TODO: not do this (SoC)
go ct.updateMarketbar() go ct.updateMarketbar()
@ -74,8 +74,8 @@ func (ct *Cointop) chartPoints(symbol string, name string) error {
// NOTE: empty list means don't show x-axis labels // NOTE: empty list means don't show x-axis labels
chart.DataLabels = []string{""} chart.DataLabels = []string{""}
rangeseconds := ct.chartrangesmap[ct.selectedchartrange] rangeseconds := ct.chartRangesMap[ct.State.selectedChartRange]
if ct.selectedchartrange == "YTD" { if ct.State.selectedChartRange == "YTD" {
ytd := time.Now().Unix() - int64(timeutil.BeginningOfYear().Unix()) ytd := time.Now().Unix() - int64(timeutil.BeginningOfYear().Unix())
rangeseconds = time.Duration(ytd) * time.Second rangeseconds = time.Duration(ytd) * time.Second
} }
@ -91,7 +91,7 @@ func (ct *Cointop) chartPoints(symbol string, name string) error {
if keyname == "" { if keyname == "" {
keyname = "globaldata" keyname = "globaldata"
} }
cachekey := ct.cacheKey(fmt.Sprintf("%s_%s", keyname, strings.Replace(ct.selectedchartrange, " ", "", -1))) cachekey := ct.cacheKey(fmt.Sprintf("%s_%s", keyname, strings.Replace(ct.State.selectedChartRange, " ", "", -1)))
cached, found := ct.cache.Get(cachekey) cached, found := ct.cache.Get(cachekey)
if found { if found {
@ -155,15 +155,15 @@ func (ct *Cointop) chartPoints(symbol string, name string) error {
points = append(points, rowpoints) points = append(points, rowpoints)
} }
ct.chartpoints = points ct.State.chartPoints = points
return nil return nil
} }
func (ct *Cointop) portfolioChart() error { func (ct *Cointop) portfolioChart() error {
maxX := ct.maxtablewidth - 3 maxX := ct.maxTableWidth - 3
chartpointslock.Lock() chartPointsLock.Lock()
defer chartpointslock.Unlock() defer chartPointsLock.Unlock()
// TODO: not do this (SoC) // TODO: not do this (SoC)
go ct.updateMarketbar() go ct.updateMarketbar()
@ -174,8 +174,8 @@ func (ct *Cointop) portfolioChart() error {
// NOTE: empty list means don't show x-axis labels // NOTE: empty list means don't show x-axis labels
chart.DataLabels = []string{""} chart.DataLabels = []string{""}
rangeseconds := ct.chartrangesmap[ct.selectedchartrange] rangeseconds := ct.chartRangesMap[ct.State.selectedChartRange]
if ct.selectedchartrange == "YTD" { if ct.State.selectedChartRange == "YTD" {
ytd := time.Now().Unix() - int64(timeutil.BeginningOfYear().Unix()) ytd := time.Now().Unix() - int64(timeutil.BeginningOfYear().Unix())
rangeseconds = time.Duration(ytd) * time.Second rangeseconds = time.Duration(ytd) * time.Second
} }
@ -201,7 +201,7 @@ func (ct *Cointop) portfolioChart() error {
} }
var graphData []float64 var graphData []float64
cachekey := strings.ToLower(fmt.Sprintf("%s_%s", p.Symbol, strings.Replace(ct.selectedchartrange, " ", "", -1))) cachekey := strings.ToLower(fmt.Sprintf("%s_%s", p.Symbol, strings.Replace(ct.State.selectedChartRange, " ", "", -1)))
cached, found := ct.cache.Get(cachekey) cached, found := ct.cache.Get(cachekey)
if found { if found {
// cache hit // cache hit
@ -263,16 +263,16 @@ func (ct *Cointop) portfolioChart() error {
points = append(points, rowpoints) points = append(points, rowpoints)
} }
ct.chartpoints = points ct.State.chartPoints = points
return nil return nil
} }
func (ct *Cointop) nextChartRange() error { func (ct *Cointop) nextChartRange() error {
sel := 0 sel := 0
max := len(ct.chartranges) max := len(ct.chartRanges)
for i, k := range ct.chartranges { for i, k := range ct.chartRanges {
if k == ct.selectedchartrange { if k == ct.State.selectedChartRange {
sel = i + 1 sel = i + 1
break break
} }
@ -281,7 +281,7 @@ func (ct *Cointop) nextChartRange() error {
sel = 0 sel = 0
} }
ct.selectedchartrange = ct.chartranges[sel] ct.State.selectedChartRange = ct.chartRanges[sel]
go ct.updateChart() go ct.updateChart()
return nil return nil
@ -289,57 +289,39 @@ func (ct *Cointop) nextChartRange() error {
func (ct *Cointop) prevChartRange() error { func (ct *Cointop) prevChartRange() error {
sel := 0 sel := 0
for i, k := range ct.chartranges { for i, k := range ct.chartRanges {
if k == ct.selectedchartrange { if k == ct.State.selectedChartRange {
sel = i - 1 sel = i - 1
break break
} }
} }
if sel < 0 { if sel < 0 {
sel = len(ct.chartranges) - 1 sel = len(ct.chartRanges) - 1
} }
ct.selectedchartrange = ct.chartranges[sel] ct.State.selectedChartRange = ct.chartRanges[sel]
go ct.updateChart() go ct.updateChart()
return nil return nil
} }
func (ct *Cointop) firstChartRange() error { func (ct *Cointop) firstChartRange() error {
ct.selectedchartrange = ct.chartranges[0] ct.State.selectedChartRange = ct.chartRanges[0]
go ct.updateChart() go ct.updateChart()
return nil return nil
} }
func (ct *Cointop) lastChartRange() error { func (ct *Cointop) lastChartRange() error {
ct.selectedchartrange = ct.chartranges[len(ct.chartranges)-1] ct.State.selectedChartRange = ct.chartRanges[len(ct.chartRanges)-1]
go ct.updateChart() go ct.updateChart()
return nil return nil
} }
func (ct *Cointop) selectedCoinName() string {
coin := ct.selectedcoin
if coin != nil {
return coin.Name
}
return ""
}
func (ct *Cointop) selectedCoinSymbol() string {
coin := ct.selectedcoin
if coin != nil {
return coin.Symbol
}
return ""
}
func (ct *Cointop) toggleCoinChart() error { func (ct *Cointop) toggleCoinChart() error {
highlightedcoin := ct.highlightedRowCoin() highlightedcoin := ct.highlightedRowCoin()
if ct.selectedcoin == highlightedcoin { if ct.State.selectedCoin == highlightedcoin {
ct.selectedcoin = nil ct.State.selectedCoin = nil
} else { } else {
ct.selectedcoin = highlightedcoin ct.State.selectedCoin = highlightedcoin
} }
go ct.updateChart() go ct.updateChart()

@ -0,0 +1,61 @@
package cointop
// Coin is the row structure
type Coin struct {
ID string
Name string
Slug string
Symbol string
Rank int
Price float64
Volume24H float64
MarketCap float64
AvailableSupply float64
TotalSupply float64
PercentChange1H float64
PercentChange24H float64
PercentChange7D float64
LastUpdated string
// for favorites
Favorite bool
// for portfolio
Holdings float64
Balance float64
}
func (ct *Cointop) allCoins() []*Coin {
if ct.State.filterByFavorites {
var list []*Coin
for i := range ct.State.allCoins {
coin := ct.State.allCoins[i]
if coin.Favorite {
list = append(list, coin)
}
}
return list
}
if ct.State.portfolioVisible {
var list []*Coin
for i := range ct.State.allCoins {
coin := ct.State.allCoins[i]
if ct.PortfolioEntryExists(coin) {
list = append(list, coin)
}
}
return list
}
return ct.State.allCoins
}
func (ct *Cointop) coinBySymbol(symbol string) *Coin {
for i := range ct.State.allCoins {
coin := ct.State.allCoins[i]
if coin.Symbol == symbol {
return coin
}
}
return nil
}

@ -24,80 +24,79 @@ import (
// ErrInvalidAPIChoice is error for invalid API choice // ErrInvalidAPIChoice is error for invalid API choice
var ErrInvalidAPIChoice = errors.New("Invalid API choice") var ErrInvalidAPIChoice = errors.New("Invalid API choice")
// Cointop cointop // Views are all views in cointop
type Cointop struct { type Views struct {
g *gocui.Gui Chart *View
apiChoice string Header *View
colorschemename string Table *View
colorscheme *Colorscheme Marketbar *View
marketbarviewname string SearchField *View
marketbarview *gocui.View Statusbar *View
chartview *gocui.View Help *View
chartviewname string ConvertMenu *View
chartpoints [][]termui.Cell Input *View
chartranges []string PortfolioUpdateMenu *View
chartrangesmap map[string]time.Duration }
selectedchartrange string
headersview *gocui.View // State is the state preferences of cointop
headerviewname string type State struct {
tableview *gocui.View allCoins []*Coin
tableviewname string allCoinsSlugMap map[string]*Coin
tablecolumnorder []string coins []*Coin
table *table.Table chartPoints [][]termui.Cell
maxtablewidth int currencyConversion string
portfoliovisible bool convertMenuVisible bool
visible bool defaultView string
statusbarview *gocui.View
statusbarviewname string
sortdesc bool
sortby string
api api.Interface
allcoins []*Coin
coins []*Coin
allcoinsslugmap map[string]*Coin
page int
perpage int
refreshmux sync.Mutex
refreshRate time.Duration
refreshTicker *time.Ticker
forcerefresh chan bool
selectedcoin *Coin
actionsmap map[string]bool
shortcutkeys map[string]string
config config // toml config
configFilepath string
searchfield *gocui.View
searchfieldviewname string
searchfieldvisible bool
// DEPRECATED: favorites by 'symbol' is deprecated because of collisions. // DEPRECATED: favorites by 'symbol' is deprecated because of collisions.
favoritesbysymbol map[string]bool favoritesbysymbol map[string]bool
favorites map[string]bool favorites map[string]bool
filterByFavorites bool filterByFavorites bool
savemux sync.Mutex helpVisible bool
cache *cache.Cache hideMarketbar bool
debug bool hideChart bool
helpview *gocui.View hideStatusbar bool
helpviewname string page int
helpvisible bool perPage int
currencyconversion string portfolio *Portfolio
convertmenuview *gocui.View portfolioVisible bool
convertmenuviewname string portfolioUpdateMenuVisible bool
convertmenuvisible bool refreshRate time.Duration
portfolio *portfolio searchFieldVisible bool
portfolioupdatemenuview *gocui.View selectedCoin *Coin
portfolioupdatemenuviewname string selectedChartRange string
portfolioupdatemenuvisible bool shortcutKeys map[string]string
inputview *gocui.View sortDesc bool
inputviewname string sortBy string
defaultView string onlyTable bool
apiKeys *apiKeys }
limiter <-chan time.Time
hideMarketbar bool // Cointop cointop
hideChart bool type Cointop struct {
hideStatusbar bool g *gocui.Gui
onlyTable bool actionsMap map[string]bool
apiKeys *APIKeys
cache *cache.Cache
config config // toml config
configFilepath string
api api.Interface
apiChoice string
chartRanges []string
chartRangesMap map[string]time.Duration
colorschemeName string
colorscheme *Colorscheme
debug bool
forceRefresh chan bool
limiter <-chan time.Time
maxTableWidth int
refreshMux sync.Mutex
refreshTicker *time.Ticker
saveMux sync.Mutex
State *State
table *table.Table
tableColumnOrder []string
Views *Views
} }
// CoinMarketCap is API choice // CoinMarketCap is API choice
@ -107,14 +106,14 @@ var CoinMarketCap = "coinmarketcap"
var CoinGecko = "coingecko" var CoinGecko = "coingecko"
// PortfolioEntry is portfolio entry // PortfolioEntry is portfolio entry
type portfolioEntry struct { type PortfolioEntry struct {
Coin string Coin string
Holdings float64 Holdings float64
} }
// Portfolio is portfolio structure // Portfolio is portfolio structure
type portfolio struct { type Portfolio struct {
Entries map[string]*portfolioEntry Entries map[string]*PortfolioEntry
} }
// Config config options // Config config options
@ -131,8 +130,8 @@ type Config struct {
RefreshRate *uint RefreshRate *uint
} }
// apiKeys is api keys structure // APIKeys is api keys structure
type apiKeys struct { type APIKeys struct {
cmc string cmc string
} }
@ -154,25 +153,14 @@ func NewCointop(config *Config) *Cointop {
} }
ct := &Cointop{ ct := &Cointop{
apiChoice: CoinGecko, apiChoice: CoinGecko,
allcoinsslugmap: make(map[string]*Coin), apiKeys: new(APIKeys),
allcoins: []*Coin{}, forceRefresh: make(chan bool),
sortby: "rank", maxTableWidth: 175,
page: 0, actionsMap: actionsMap(),
perpage: 100, cache: cache.New(1*time.Minute, 2*time.Minute),
forcerefresh: make(chan bool), configFilepath: configFilepath,
maxtablewidth: 175, chartRanges: []string{
actionsmap: actionsMap(),
shortcutkeys: defaultShortcuts(),
// DEPRECATED: favorites by 'symbol' is deprecated because of collisions. Kept for backward compatibility.
favoritesbysymbol: make(map[string]bool),
favorites: make(map[string]bool),
cache: cache.New(1*time.Minute, 2*time.Minute),
debug: debug,
configFilepath: configFilepath,
marketbarviewname: "market",
chartviewname: "chart",
chartranges: []string{
"1H", "1H",
"6H", "6H",
"24H", "24H",
@ -185,7 +173,8 @@ func NewCointop(config *Config) *Cointop {
"YTD", "YTD",
"All Time", "All Time",
}, },
chartrangesmap: map[string]time.Duration{ debug: debug,
chartRangesMap: map[string]time.Duration{
"All Time": time.Duration(24 * 7 * 4 * 12 * 5 * time.Hour), "All Time": time.Duration(24 * 7 * 4 * 12 * 5 * time.Hour),
"YTD": time.Duration(1 * time.Second), // this will be calculated "YTD": time.Duration(1 * time.Second), // this will be calculated
"1Y": time.Duration(24 * 7 * 4 * 12 * time.Hour), "1Y": time.Duration(24 * 7 * 4 * 12 * time.Hour),
@ -198,10 +187,29 @@ func NewCointop(config *Config) *Cointop {
"6H": time.Duration(6 * time.Hour), "6H": time.Duration(6 * time.Hour),
"1H": time.Duration(1 * time.Hour), "1H": time.Duration(1 * time.Hour),
}, },
selectedchartrange: "7D", limiter: time.Tick(2 * time.Second),
headerviewname: "header", State: &State{
tableviewname: "table", allCoinsSlugMap: make(map[string]*Coin),
tablecolumnorder: []string{ allCoins: []*Coin{},
currencyConversion: "USD",
// DEPRECATED: favorites by 'symbol' is deprecated because of collisions. Kept for backward compatibility.
favoritesbysymbol: make(map[string]bool),
favorites: make(map[string]bool),
hideMarketbar: config.HideMarketbar,
hideChart: config.HideChart,
hideStatusbar: config.HideStatusbar,
onlyTable: config.OnlyTable,
refreshRate: 60 * time.Second,
selectedChartRange: "7D",
shortcutKeys: defaultShortcuts(),
sortBy: "rank",
page: 0,
perPage: 100,
portfolio: &Portfolio{
Entries: make(map[string]*PortfolioEntry, 0),
},
},
tableColumnOrder: []string{
"rank", "rank",
"name", "name",
"symbol", "symbol",
@ -217,23 +225,38 @@ func NewCointop(config *Config) *Cointop {
"percentholdings", "percentholdings",
"lastupdated", "lastupdated",
}, },
statusbarviewname: "statusbar", Views: &Views{
searchfieldviewname: "searchfield", Chart: &View{
helpviewname: "help", Name: "chart",
convertmenuviewname: "convertmenu", },
currencyconversion: "USD", Header: &View{
portfolio: &portfolio{ Name: "header",
Entries: make(map[string]*portfolioEntry, 0), },
Table: &View{
Name: "table",
},
Marketbar: &View{
Name: "marketbar",
},
SearchField: &View{
Name: "searchfield",
},
Statusbar: &View{
Name: "statusbar",
},
Help: &View{
Name: "help",
},
ConvertMenu: &View{
Name: "convert",
},
Input: &View{
Name: "input",
},
PortfolioUpdateMenu: &View{
Name: "portfolioupdatemenu",
},
}, },
portfolioupdatemenuviewname: "portfolioupdatemenu",
inputviewname: "input",
apiKeys: new(apiKeys),
limiter: time.Tick(2 * time.Second),
hideMarketbar: config.HideMarketbar,
hideChart: config.HideChart,
hideStatusbar: config.HideStatusbar,
onlyTable: config.OnlyTable,
refreshRate: 60 * time.Second,
} }
err := ct.setupConfig() err := ct.setupConfig()
@ -241,20 +264,20 @@ func NewCointop(config *Config) *Cointop {
log.Fatal(err) log.Fatal(err)
} }
ct.cache.Set("onlyTable", ct.onlyTable, cache.NoExpiration) ct.cache.Set("onlyTable", ct.State.onlyTable, cache.NoExpiration)
ct.cache.Set("hideMarketbar", ct.hideMarketbar, cache.NoExpiration) ct.cache.Set("hideMarketbar", ct.State.hideMarketbar, cache.NoExpiration)
ct.cache.Set("hideChart", ct.hideChart, cache.NoExpiration) ct.cache.Set("hideChart", ct.State.hideChart, cache.NoExpiration)
ct.cache.Set("hideStatusbar", ct.hideStatusbar, cache.NoExpiration) ct.cache.Set("hideStatusbar", ct.State.hideStatusbar, cache.NoExpiration)
if config.RefreshRate != nil { if config.RefreshRate != nil {
ct.refreshRate = time.Duration(*config.RefreshRate) * time.Second ct.State.refreshRate = time.Duration(*config.RefreshRate) * time.Second
} }
if ct.refreshRate == 0 { if ct.State.refreshRate == 0 {
ct.refreshTicker = time.NewTicker(time.Duration(1)) ct.refreshTicker = time.NewTicker(time.Duration(1))
ct.refreshTicker.Stop() ct.refreshTicker.Stop()
} else { } else {
ct.refreshTicker = time.NewTicker(ct.refreshRate) ct.refreshTicker = time.NewTicker(ct.State.refreshRate)
} }
// prompt for CoinMarketCap api key if not found // prompt for CoinMarketCap api key if not found
@ -266,7 +289,7 @@ func NewCointop(config *Config) *Cointop {
} }
if config.Colorscheme != "" { if config.Colorscheme != "" {
ct.colorschemename = config.Colorscheme ct.colorschemeName = config.Colorscheme
} }
colors, err := ct.getColorschemeColors() colors, err := ct.getColorschemeColors()
@ -297,7 +320,7 @@ func NewCointop(config *Config) *Cointop {
} }
if ct.apiChoice == CoinGecko { if ct.apiChoice == CoinGecko {
ct.selectedchartrange = "1Y" ct.State.selectedChartRange = "1Y"
} }
if ct.apiChoice == CoinMarketCap { if ct.apiChoice == CoinMarketCap {
@ -308,35 +331,35 @@ func NewCointop(config *Config) *Cointop {
log.Fatal(ErrInvalidAPIChoice) log.Fatal(ErrInvalidAPIChoice)
} }
coinscachekey := ct.cacheKey("allcoinsslugmap") coinscachekey := ct.cacheKey("allCoinsSlugMap")
filecache.Get(coinscachekey, &ct.allcoinsslugmap) filecache.Get(coinscachekey, &ct.State.allCoinsSlugMap)
for k := range ct.allcoinsslugmap { for k := range ct.State.allCoinsSlugMap {
ct.allcoins = append(ct.allcoins, ct.allcoinsslugmap[k]) ct.State.allCoins = append(ct.State.allCoins, ct.State.allCoinsSlugMap[k])
} }
if len(ct.allcoins) > 1 { if len(ct.State.allCoins) > 1 {
max := len(ct.allcoins) max := len(ct.State.allCoins)
if max > 100 { if max > 100 {
max = 100 max = 100
} }
ct.sort(ct.sortby, ct.sortdesc, ct.allcoins, false) ct.sort(ct.State.sortBy, ct.State.sortDesc, ct.State.allCoins, false)
ct.coins = ct.allcoins[0:max] ct.State.coins = ct.State.allCoins[0:max]
} }
// DEPRECATED: favorites by 'symbol' is deprecated because of collisions. Kept for backward compatibility. // DEPRECATED: favorites by 'symbol' is deprecated because of collisions. Kept for backward compatibility.
// Here we're doing a lookup based on symbol and setting the favorite to the coin name instead of coin symbol. // Here we're doing a lookup based on symbol and setting the favorite to the coin name instead of coin symbol.
for i := range ct.allcoinsslugmap { for i := range ct.State.allCoinsSlugMap {
coin := ct.allcoinsslugmap[i] coin := ct.State.allCoinsSlugMap[i]
for k := range ct.favoritesbysymbol { for k := range ct.State.favoritesbysymbol {
if coin.Symbol == k { if coin.Symbol == k {
ct.favorites[coin.Name] = true ct.State.favorites[coin.Name] = true
delete(ct.favoritesbysymbol, k) delete(ct.State.favoritesbysymbol, k)
} }
} }
} }
var globaldata []float64 var globaldata []float64
chartcachekey := ct.cacheKey(fmt.Sprintf("%s_%s", "globaldata", strings.Replace(ct.selectedchartrange, " ", "", -1))) chartcachekey := ct.cacheKey(fmt.Sprintf("%s_%s", "globaldata", strings.Replace(ct.State.selectedChartRange, " ", "", -1)))
filecache.Get(chartcachekey, &globaldata) filecache.Get(chartcachekey, &globaldata)
ct.cache.Set(chartcachekey, globaldata, 10*time.Second) ct.cache.Set(chartcachekey, globaldata, 10*time.Second)

@ -8,6 +8,8 @@ import (
xtermcolor "github.com/tomnomnom/xtermcolor" xtermcolor "github.com/tomnomnom/xtermcolor"
) )
// TODO: fix hex color support
// colorschemeColors .. // colorschemeColors ..
type colorschemeColors map[string]interface{} type colorschemeColors map[string]interface{}

@ -125,8 +125,8 @@ func (ct *Cointop) makeConfigFile() error {
} }
func (ct *Cointop) saveConfig() error { func (ct *Cointop) saveConfig() error {
ct.savemux.Lock() ct.saveMux.Lock()
defer ct.savemux.Unlock() defer ct.saveMux.Unlock()
path := ct.configPath() path := ct.configPath()
if _, err := os.Stat(path); err == nil { if _, err := os.Stat(path); err == nil {
b, err := ct.configToToml() b, err := ct.configToToml()
@ -154,13 +154,13 @@ func (ct *Cointop) parseConfig() error {
func (ct *Cointop) configToToml() ([]byte, error) { func (ct *Cointop) configToToml() ([]byte, error) {
shortcutsIfcs := map[string]interface{}{} shortcutsIfcs := map[string]interface{}{}
for k, v := range ct.shortcutkeys { for k, v := range ct.State.shortcutKeys {
var i interface{} = v var i interface{} = v
shortcutsIfcs[k] = i shortcutsIfcs[k] = i
} }
var favorites []interface{} var favorites []interface{}
for k, ok := range ct.favorites { for k, ok := range ct.State.favorites {
if ok { if ok {
var i interface{} = k var i interface{} = k
favorites = append(favorites, i) favorites = append(favorites, i)
@ -174,8 +174,8 @@ func (ct *Cointop) configToToml() ([]byte, error) {
} }
portfolioIfc := map[string]interface{}{} portfolioIfc := map[string]interface{}{}
for name := range ct.portfolio.Entries { for name := range ct.State.portfolio.Entries {
entry, ok := ct.portfolio.Entries[name] entry, ok := ct.State.portfolio.Entries[name]
if !ok || entry.Coin == "" { if !ok || entry.Coin == "" {
continue continue
} }
@ -183,10 +183,10 @@ func (ct *Cointop) configToToml() ([]byte, error) {
portfolioIfc[entry.Coin] = i portfolioIfc[entry.Coin] = i
} }
var currencyIfc interface{} = ct.currencyconversion var currencyIfc interface{} = ct.State.currencyConversion
var defaultViewIfc interface{} = ct.defaultView var defaultViewIfc interface{} = ct.State.defaultView
var colorschemeIfc interface{} = ct.colorschemename var colorschemeIfc interface{} = ct.colorschemeName
var refreshRateIfc interface{} = uint(ct.refreshRate.Seconds()) var refreshRateIfc interface{} = uint(ct.State.refreshRate.Seconds())
cmcIfc := map[string]interface{}{ cmcIfc := map[string]interface{}{
"pro_api_key": ct.apiKeys.cmc, "pro_api_key": ct.apiKeys.cmc,
@ -221,10 +221,10 @@ func (ct *Cointop) loadShortcutsFromConfig() error {
if !ct.actionExists(v) { if !ct.actionExists(v) {
continue continue
} }
if ct.shortcutkeys[k] == "" { if ct.State.shortcutKeys[k] == "" {
continue continue
} }
ct.shortcutkeys[k] = v ct.State.shortcutKeys[k] = v
} }
} }
return nil return nil
@ -232,7 +232,7 @@ func (ct *Cointop) loadShortcutsFromConfig() error {
func (ct *Cointop) loadCurrencyFromConfig() error { func (ct *Cointop) loadCurrencyFromConfig() error {
if currency, ok := ct.config.Currency.(string); ok { if currency, ok := ct.config.Currency.(string); ok {
ct.currencyconversion = strings.ToUpper(currency) ct.State.currencyConversion = strings.ToUpper(currency)
} }
return nil return nil
} }
@ -242,17 +242,17 @@ func (ct *Cointop) loadDefaultViewFromConfig() error {
defaultView = strings.ToLower(defaultView) defaultView = strings.ToLower(defaultView)
switch defaultView { switch defaultView {
case "portfolio": case "portfolio":
ct.portfoliovisible = true ct.State.portfolioVisible = true
case "favorites": case "favorites":
ct.filterByFavorites = true ct.State.filterByFavorites = true
case "default": case "default":
fallthrough fallthrough
default: default:
ct.portfoliovisible = false ct.State.portfolioVisible = false
ct.filterByFavorites = false ct.State.filterByFavorites = false
defaultView = "default" defaultView = "default"
} }
ct.defaultView = defaultView ct.State.defaultView = defaultView
} }
return nil return nil
} }
@ -269,7 +269,7 @@ func (ct *Cointop) loadAPIKeysFromConfig() error {
func (ct *Cointop) loadColorschemeFromConfig() error { func (ct *Cointop) loadColorschemeFromConfig() error {
if colorscheme, ok := ct.config.Colorscheme.(string); ok { if colorscheme, ok := ct.config.Colorscheme.(string); ok {
ct.colorschemename = colorscheme ct.colorschemeName = colorscheme
} }
return nil return nil
@ -277,7 +277,7 @@ func (ct *Cointop) loadColorschemeFromConfig() error {
func (ct *Cointop) loadRefreshRateFromConfig() error { func (ct *Cointop) loadRefreshRateFromConfig() error {
if refreshRate, ok := ct.config.RefreshRate.(int64); ok { if refreshRate, ok := ct.config.RefreshRate.(int64); ok {
ct.refreshRate = time.Duration(uint(refreshRate)) * time.Second ct.State.refreshRate = time.Duration(uint(refreshRate)) * time.Second
} }
return nil return nil
@ -285,16 +285,16 @@ func (ct *Cointop) loadRefreshRateFromConfig() error {
func (ct *Cointop) getColorschemeColors() (map[string]interface{}, error) { func (ct *Cointop) getColorschemeColors() (map[string]interface{}, error) {
var colors map[string]interface{} var colors map[string]interface{}
if ct.colorschemename == "" { if ct.colorschemeName == "" {
ct.colorschemename = defaultColorscheme ct.colorschemeName = defaultColorscheme
if _, err := toml.Decode(DefaultColors, &colors); err != nil { if _, err := toml.Decode(DefaultColors, &colors); err != nil {
return nil, err return nil, err
} }
} else { } else {
path := normalizePath(fmt.Sprintf("~/.cointop/colors/%s.toml", ct.colorschemename)) path := normalizePath(fmt.Sprintf("~/.cointop/colors/%s.toml", ct.colorschemeName))
if _, err := os.Stat(path); os.IsNotExist(err) { if _, err := os.Stat(path); os.IsNotExist(err) {
// NOTE: case for when cointop is set as the theme but the colorscheme file doesn't exist // NOTE: case for when cointop is set as the theme but the colorscheme file doesn't exist
if ct.colorschemename == "cointop" { if ct.colorschemeName == "cointop" {
if _, err := toml.Decode(DefaultColors, &colors); err != nil { if _, err := toml.Decode(DefaultColors, &colors); err != nil {
return nil, err return nil, err
} }
@ -328,13 +328,13 @@ func (ct *Cointop) loadFavoritesFromConfig() error {
if k == "symbols" { if k == "symbols" {
for _, ifc := range arr { for _, ifc := range arr {
if v, ok := ifc.(string); ok { if v, ok := ifc.(string); ok {
ct.favoritesbysymbol[strings.ToUpper(v)] = true ct.State.favoritesbysymbol[strings.ToUpper(v)] = true
} }
} }
} else if k == "names" { } else if k == "names" {
for _, ifc := range arr { for _, ifc := range arr {
if v, ok := ifc.(string); ok { if v, ok := ifc.(string); ok {
ct.favorites[v] = true ct.State.favorites[v] = true
} }
} }
} }

@ -119,19 +119,11 @@ func (ct *Cointop) sortedSupportedCurrencyConversions() []string {
return 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() { func (ct *Cointop) updateConvertMenu() {
header := ct.colorscheme.MenuHeader(fmt.Sprintf(" Currency Conversion %s\n\n", pad.Left("[q] close menu ", ct.maxtablewidth-20, " "))) header := ct.colorscheme.MenuHeader(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" helpline := " Press the corresponding key to select currency for conversion\n\n"
cnt := 0 cnt := 0
h := ct.viewHeight(ct.convertmenuviewname) h := ct.viewHeight(ct.Views.ConvertMenu.Name)
percol := h - 5 percol := h - 5
cols := make([][]string, percol) cols := make([][]string, percol)
for i := range cols { for i := range cols {
@ -146,7 +138,7 @@ func (ct *Cointop) updateConvertMenu() {
cnt = 0 cnt = 0
} }
shortcut := string(alphanumericcharacters[i]) shortcut := string(alphanumericcharacters[i])
if key == ct.currencyconversion { if key == ct.State.currencyConversion {
shortcut = ct.colorscheme.MenuLabelActive(color.Bold("*")) shortcut = ct.colorscheme.MenuLabelActive(color.Bold("*"))
key = ct.colorscheme.Menu(color.Bold(key)) key = ct.colorscheme.Menu(color.Bold(key))
currency = ct.colorscheme.MenuLabelActive(color.Bold(currency)) currency = ct.colorscheme.MenuLabelActive(color.Bold(currency))
@ -171,53 +163,61 @@ func (ct *Cointop) updateConvertMenu() {
content := fmt.Sprintf("%s%s%s", header, helpline, body) content := fmt.Sprintf("%s%s%s", header, helpline, body)
ct.update(func() { ct.update(func() {
if ct.convertmenuview == nil { if ct.Views.ConvertMenu.Backing == nil {
return return
} }
ct.convertmenuview.Clear() ct.Views.ConvertMenu.Backing.Clear()
ct.convertmenuview.Frame = true ct.Views.ConvertMenu.Backing.Frame = true
fmt.Fprintln(ct.convertmenuview, content) fmt.Fprintln(ct.Views.ConvertMenu.Backing, content)
}) })
} }
func (ct *Cointop) setCurrencyConverstion(convert string) func() error {
return func() error {
ct.State.currencyConversion = convert
ct.hideConvertMenu()
go ct.refreshAll()
return nil
}
}
func (ct *Cointop) currencySymbol() string {
symbol, ok := currencySymbol[ct.State.currencyConversion]
if ok {
return symbol
}
return "$"
}
func (ct *Cointop) showConvertMenu() error { func (ct *Cointop) showConvertMenu() error {
ct.convertmenuvisible = true ct.State.convertMenuVisible = true
ct.updateConvertMenu() ct.updateConvertMenu()
ct.setActiveView(ct.convertmenuviewname) ct.setActiveView(ct.Views.ConvertMenu.Name)
return nil return nil
} }
func (ct *Cointop) hideConvertMenu() error { func (ct *Cointop) hideConvertMenu() error {
ct.convertmenuvisible = false ct.State.convertMenuVisible = false
ct.setViewOnBottom(ct.convertmenuviewname) ct.setViewOnBottom(ct.Views.ConvertMenu.Name)
ct.setActiveView(ct.tableviewname) ct.setActiveView(ct.Views.Table.Name)
ct.update(func() { ct.update(func() {
if ct.convertmenuview == nil { if ct.Views.ConvertMenu.Backing == nil {
return return
} }
ct.convertmenuview.Clear() ct.Views.ConvertMenu.Backing.Clear()
ct.convertmenuview.Frame = false ct.Views.ConvertMenu.Backing.Frame = false
fmt.Fprintln(ct.convertmenuview, "") fmt.Fprintln(ct.Views.ConvertMenu.Backing, "")
}) })
return nil return nil
} }
func (ct *Cointop) setCurrencyConverstion(convert string) func() error { func (ct *Cointop) toggleConvertMenu() error {
return func() error { ct.State.convertMenuVisible = !ct.State.convertMenuVisible
ct.currencyconversion = convert if ct.State.convertMenuVisible {
ct.hideConvertMenu() return ct.showConvertMenu()
go ct.refreshAll()
return nil
}
}
func (ct *Cointop) currencySymbol() string {
symbol, ok := currencySymbol[ct.currencyconversion]
if ok {
return symbol
} }
return ct.hideConvertMenu()
return "$"
} }

@ -1,17 +1,17 @@
package cointop package cointop
func (ct *Cointop) toggleFavorite() error { func (ct *Cointop) toggleFavorite() error {
ct.portfoliovisible = false ct.State.portfolioVisible = false
coin := ct.highlightedRowCoin() coin := ct.highlightedRowCoin()
if coin == nil { if coin == nil {
return nil return nil
} }
_, ok := ct.favorites[coin.Name] _, ok := ct.State.favorites[coin.Name]
if ok { if ok {
delete(ct.favorites, coin.Name) delete(ct.State.favorites, coin.Name)
coin.Favorite = false coin.Favorite = false
} else { } else {
ct.favorites[coin.Name] = true ct.State.favorites[coin.Name] = true
coin.Favorite = true coin.Favorite = true
} }
go ct.updateTable() go ct.updateTable()
@ -19,8 +19,8 @@ func (ct *Cointop) toggleFavorite() error {
} }
func (ct *Cointop) toggleShowFavorites() error { func (ct *Cointop) toggleShowFavorites() error {
ct.portfoliovisible = false ct.State.portfolioVisible = false
ct.filterByFavorites = !ct.filterByFavorites ct.State.filterByFavorites = !ct.State.filterByFavorites
go ct.updateTable() go ct.updateTable()
return nil return nil
} }

@ -7,31 +7,23 @@ import (
"github.com/miguelmota/cointop/cointop/common/pad" "github.com/miguelmota/cointop/cointop/common/pad"
) )
func (ct *Cointop) toggleHelp() error {
ct.helpvisible = !ct.helpvisible
if ct.helpvisible {
return ct.showHelp()
}
return ct.hideHelp()
}
func (ct *Cointop) updateHelp() { func (ct *Cointop) updateHelp() {
keys := make([]string, 0, len(ct.shortcutkeys)) keys := make([]string, 0, len(ct.State.shortcutKeys))
for k := range ct.shortcutkeys { for k := range ct.State.shortcutKeys {
keys = append(keys, k) keys = append(keys, k)
} }
sort.Strings(keys) sort.Strings(keys)
header := ct.colorscheme.MenuHeader(fmt.Sprintf(" Help %s\n\n", pad.Left("[q] close ", ct.maxtablewidth-10, " "))) header := ct.colorscheme.MenuHeader(fmt.Sprintf(" Help %s\n\n", pad.Left("[q] close ", ct.maxTableWidth-10, " ")))
cnt := 0 cnt := 0
h := ct.viewHeight(ct.helpviewname) h := ct.viewHeight(ct.Views.Help.Name)
percol := h - 6 percol := h - 6
cols := make([][]string, percol) cols := make([][]string, percol)
for i := range cols { for i := range cols {
cols[i] = make([]string, 20) cols[i] = make([]string, 20)
} }
for _, k := range keys { for _, k := range keys {
v := ct.shortcutkeys[k] v := ct.State.shortcutKeys[k]
if cnt%percol == 0 { if cnt%percol == 0 {
cnt = 0 cnt = 0
} }
@ -52,39 +44,47 @@ func (ct *Cointop) updateHelp() {
body = fmt.Sprintf("%s\n", body) body = fmt.Sprintf("%s\n", body)
infoline := " List of keyboard shortcuts\n\n" infoline := " List of keyboard shortcuts\n\n"
versionline := pad.Left(fmt.Sprintf("v%s", ct.version()), ct.maxtablewidth-5, " ") versionline := pad.Left(fmt.Sprintf("v%s", ct.version()), ct.maxTableWidth-5, " ")
content := header + infoline + body + versionline content := header + infoline + body + versionline
ct.update(func() { ct.update(func() {
if ct.helpview == nil { if ct.Views.Help.Backing == nil {
return return
} }
ct.helpview.Clear() ct.Views.Help.Backing.Clear()
ct.helpview.Frame = true ct.Views.Help.Backing.Frame = true
fmt.Fprintln(ct.helpview, content) fmt.Fprintln(ct.Views.Help.Backing, content)
}) })
} }
func (ct *Cointop) showHelp() error { func (ct *Cointop) showHelp() error {
ct.helpvisible = true ct.State.helpVisible = true
ct.updateHelp() ct.updateHelp()
ct.setActiveView(ct.helpviewname) ct.setActiveView(ct.Views.Help.Name)
return nil return nil
} }
func (ct *Cointop) hideHelp() error { func (ct *Cointop) hideHelp() error {
ct.helpvisible = false ct.State.helpVisible = false
ct.setViewOnBottom(ct.helpviewname) ct.setViewOnBottom(ct.Views.Help.Name)
ct.setActiveView(ct.tableviewname) ct.setActiveView(ct.Views.Table.Name)
ct.update(func() { ct.update(func() {
if ct.helpview == nil { if ct.Views.Help.Backing == nil {
return return
} }
ct.helpview.Clear() ct.Views.Help.Backing.Clear()
ct.helpview.Frame = false ct.Views.Help.Backing.Frame = false
fmt.Fprintln(ct.helpview, "") fmt.Fprintln(ct.Views.Help.Backing, "")
}) })
return nil return nil
} }
func (ct *Cointop) toggleHelp() error {
ct.State.helpVisible = !ct.State.helpVisible
if ct.State.helpVisible {
return ct.showHelp()
}
return ct.hideHelp()
}

@ -207,7 +207,7 @@ func (ct *Cointop) parseKeys(s string) (interface{}, gocui.Modifier) {
} }
func (ct *Cointop) keybindings(g *gocui.Gui) error { func (ct *Cointop) keybindings(g *gocui.Gui) error {
for k, v := range ct.shortcutkeys { for k, v := range ct.State.shortcutKeys {
if k == "" { if k == "" {
continue continue
} }
@ -345,29 +345,29 @@ func (ct *Cointop) keybindings(g *gocui.Gui) error {
ct.setKeybindingMod(gocui.KeyCtrlZ, gocui.ModNone, ct.keyfn(ct.quit), "") ct.setKeybindingMod(gocui.KeyCtrlZ, gocui.ModNone, ct.keyfn(ct.quit), "")
// searchfield keys // searchfield keys
ct.setKeybindingMod(gocui.KeyEnter, gocui.ModNone, ct.keyfn(ct.doSearch), ct.searchfieldviewname) ct.setKeybindingMod(gocui.KeyEnter, gocui.ModNone, ct.keyfn(ct.doSearch), ct.Views.SearchField.Name)
ct.setKeybindingMod(gocui.KeyEsc, gocui.ModNone, ct.keyfn(ct.cancelSearch), ct.searchfieldviewname) ct.setKeybindingMod(gocui.KeyEsc, gocui.ModNone, ct.keyfn(ct.cancelSearch), ct.Views.SearchField.Name)
// keys to quit help when open // keys to quit help when open
ct.setKeybindingMod(gocui.KeyEsc, gocui.ModNone, ct.keyfn(ct.hideHelp), ct.helpviewname) ct.setKeybindingMod(gocui.KeyEsc, gocui.ModNone, ct.keyfn(ct.hideHelp), ct.Views.Help.Name)
ct.setKeybindingMod('q', gocui.ModNone, ct.keyfn(ct.hideHelp), ct.helpviewname) ct.setKeybindingMod('q', gocui.ModNone, ct.keyfn(ct.hideHelp), ct.Views.Help.Name)
// keys to quit portfolio update menu when open // keys to quit portfolio update menu when open
ct.setKeybindingMod(gocui.KeyEsc, gocui.ModNone, ct.keyfn(ct.hidePortfolioUpdateMenu), ct.inputviewname) ct.setKeybindingMod(gocui.KeyEsc, gocui.ModNone, ct.keyfn(ct.hidePortfolioUpdateMenu), ct.Views.Input.Name)
ct.setKeybindingMod('q', gocui.ModNone, ct.keyfn(ct.hidePortfolioUpdateMenu), ct.inputviewname) ct.setKeybindingMod('q', gocui.ModNone, ct.keyfn(ct.hidePortfolioUpdateMenu), ct.Views.Input.Name)
// keys to update portfolio holdings // keys to update portfolio holdings
ct.setKeybindingMod(gocui.KeyEnter, gocui.ModNone, ct.keyfn(ct.setPortfolioHoldings), ct.inputviewname) ct.setKeybindingMod(gocui.KeyEnter, gocui.ModNone, ct.keyfn(ct.setPortfolioHoldings), ct.Views.Input.Name)
// keys to quit convert menu when open // keys to quit convert menu when open
ct.setKeybindingMod(gocui.KeyEsc, gocui.ModNone, ct.keyfn(ct.hideConvertMenu), ct.convertmenuviewname) ct.setKeybindingMod(gocui.KeyEsc, gocui.ModNone, ct.keyfn(ct.hideConvertMenu), ct.Views.ConvertMenu.Name)
ct.setKeybindingMod('q', gocui.ModNone, ct.keyfn(ct.hideConvertMenu), ct.convertmenuviewname) ct.setKeybindingMod('q', gocui.ModNone, ct.keyfn(ct.hideConvertMenu), ct.Views.ConvertMenu.Name)
// character key press to select option // character key press to select option
// TODO: use scrolling table // TODO: use scrolling table
keys := ct.sortedSupportedCurrencyConversions() keys := ct.sortedSupportedCurrencyConversions()
for i, k := range keys { for i, k := range keys {
ct.setKeybindingMod(rune(alphanumericcharacters[i]), gocui.ModNone, ct.keyfn(ct.setCurrencyConverstion(k)), ct.convertmenuviewname) ct.setKeybindingMod(rune(alphanumericcharacters[i]), gocui.ModNone, ct.keyfn(ct.setCurrencyConverstion(k)), ct.Views.ConvertMenu.Name)
} }
return nil return nil
@ -392,7 +392,7 @@ func (ct *Cointop) keyfn(fn func() error) func(g *gocui.Gui, v *gocui.View) erro
func (ct *Cointop) handleHkey(key interface{}) func(g *gocui.Gui, v *gocui.View) error { func (ct *Cointop) handleHkey(key interface{}) func(g *gocui.Gui, v *gocui.View) error {
return func(g *gocui.Gui, v *gocui.View) error { return func(g *gocui.Gui, v *gocui.View) error {
if k, ok := key.(rune); ok && k == 'h' && ct.portfoliovisible { if k, ok := key.(rune); ok && k == 'h' && ct.State.portfolioVisible {
ct.sortToggle("holdings", true) ct.sortToggle("holdings", true)
} else { } else {
ct.prevPage() ct.prevPage()

@ -7,6 +7,8 @@ import (
"github.com/jroimartin/gocui" "github.com/jroimartin/gocui"
) )
// TODO: break up into small functions
// layout sets initial layout // layout sets initial layout
func (ct *Cointop) layout(g *gocui.Gui) error { func (ct *Cointop) layout(g *gocui.Gui) error {
maxX, maxY := ct.size() maxX, maxY := ct.size()
@ -17,63 +19,63 @@ func (ct *Cointop) layout(g *gocui.Gui) error {
chartHeight := 10 chartHeight := 10
statusbarHeight := 1 statusbarHeight := 1
if ct.onlyTable { if ct.State.onlyTable {
ct.hideMarketbar = true ct.State.hideMarketbar = true
ct.hideChart = true ct.State.hideChart = true
ct.hideStatusbar = true ct.State.hideStatusbar = true
} }
if ct.hideMarketbar { if ct.State.hideMarketbar {
marketbarHeight = 0 marketbarHeight = 0
} }
if ct.hideChart { if ct.State.hideChart {
chartHeight = 0 chartHeight = 0
} }
if ct.hideStatusbar { if ct.State.hideStatusbar {
statusbarHeight = 0 statusbarHeight = 0
} }
if !ct.hideMarketbar { if !ct.State.hideMarketbar {
if v, err := g.SetView(ct.marketbarviewname, 0, topOffset, maxX, 2); err != nil { if v, err := g.SetView(ct.Views.Marketbar.Name, 0, topOffset, maxX, 2); err != nil {
if err != gocui.ErrUnknownView { if err != gocui.ErrUnknownView {
return err return err
} }
ct.marketbarview = v ct.Views.Marketbar.Backing = v
ct.marketbarview.Frame = false ct.Views.Marketbar.Backing.Frame = false
ct.colorscheme.SetViewColor(ct.marketbarview, "marketbar") ct.colorscheme.SetViewColor(ct.Views.Marketbar.Backing, "marketbar")
go func() { go func() {
ct.updateMarketbar() ct.updateMarketbar()
_, found := ct.cache.Get(ct.marketbarviewname) _, found := ct.cache.Get(ct.Views.Marketbar.Name)
if found { if found {
ct.cache.Delete(ct.marketbarviewname) ct.cache.Delete(ct.Views.Marketbar.Name)
ct.updateMarketbar() ct.updateMarketbar()
} }
}() }()
} }
} else { } else {
if ct.marketbarview != nil { if ct.Views.Marketbar.Backing != nil {
if err := g.DeleteView(ct.marketbarviewname); err != nil { if err := g.DeleteView(ct.Views.Marketbar.Name); err != nil {
return err return err
} }
ct.marketbarview = nil ct.Views.Marketbar.Backing = nil
} }
} }
topOffset = topOffset + marketbarHeight topOffset = topOffset + marketbarHeight
if !ct.hideChart { if !ct.State.hideChart {
if v, err := g.SetView(ct.chartviewname, 0, topOffset, maxX, topOffset+chartHeight+marketbarHeight); err != nil { if v, err := g.SetView(ct.Views.Chart.Name, 0, topOffset, maxX, topOffset+chartHeight+marketbarHeight); err != nil {
if err != gocui.ErrUnknownView { if err != gocui.ErrUnknownView {
return err return err
} }
ct.chartview = v ct.Views.Chart.Backing = v
ct.chartview.Frame = false ct.Views.Chart.Backing.Frame = false
ct.colorscheme.SetViewColor(ct.chartview, "chart") ct.colorscheme.SetViewColor(ct.Views.Chart.Backing, "chart")
go func() { go func() {
ct.updateChart() ct.updateChart()
cachekey := strings.ToLower(fmt.Sprintf("%s_%s", "globaldata", strings.Replace(ct.selectedchartrange, " ", "", -1))) cachekey := strings.ToLower(fmt.Sprintf("%s_%s", "globaldata", strings.Replace(ct.State.selectedChartRange, " ", "", -1)))
_, found := ct.cache.Get(cachekey) _, found := ct.cache.Get(cachekey)
if found { if found {
ct.cache.Delete(cachekey) ct.cache.Delete(cachekey)
@ -82,37 +84,37 @@ func (ct *Cointop) layout(g *gocui.Gui) error {
}() }()
} }
} else { } else {
if ct.chartview != nil { if ct.Views.Chart.Backing != nil {
if err := g.DeleteView(ct.chartviewname); err != nil { if err := g.DeleteView(ct.Views.Chart.Name); err != nil {
return err return err
} }
ct.chartview = nil ct.Views.Chart.Backing = nil
} }
} }
topOffset = topOffset + chartHeight topOffset = topOffset + chartHeight
if v, err := g.SetView(ct.headerviewname, 0, topOffset, ct.maxtablewidth, topOffset+2); err != nil { if v, err := g.SetView(ct.Views.Header.Name, 0, topOffset, ct.maxTableWidth, topOffset+2); err != nil {
if err != gocui.ErrUnknownView { if err != gocui.ErrUnknownView {
return err return err
} }
ct.headersview = v ct.Views.Header.Backing = v
ct.headersview.Frame = false ct.Views.Header.Backing.Frame = false
ct.colorscheme.SetViewColor(ct.headersview, "table_header") ct.colorscheme.SetViewColor(ct.Views.Header.Backing, "table_header")
go ct.updateHeaders() go ct.updateHeaders()
} }
topOffset = topOffset + headerHeight topOffset = topOffset + headerHeight
if v, err := g.SetView(ct.tableviewname, 0, topOffset, ct.maxtablewidth, maxY-statusbarHeight); err != nil { if v, err := g.SetView(ct.Views.Table.Name, 0, topOffset, ct.maxTableWidth, maxY-statusbarHeight); err != nil {
if err != gocui.ErrUnknownView { if err != gocui.ErrUnknownView {
return err return err
} }
ct.tableview = v ct.Views.Table.Backing = v
ct.tableview.Frame = false ct.Views.Table.Backing.Frame = false
ct.tableview.Highlight = true ct.Views.Table.Backing.Highlight = true
ct.colorscheme.SetViewActiveColor(ct.tableview, "table_row_active") ct.colorscheme.SetViewActiveColor(ct.Views.Table.Backing, "table_row_active")
_, found := ct.cache.Get("allcoinsslugmap") _, found := ct.cache.Get("allCoinsSlugMap")
if found { if found {
ct.cache.Delete("allcoinsslugmap") ct.cache.Delete("allCoinsSlugMap")
} }
go func() { go func() {
ct.updateCoins() ct.updateCoins()
@ -120,123 +122,84 @@ func (ct *Cointop) layout(g *gocui.Gui) error {
}() }()
} }
if !ct.hideStatusbar { if !ct.State.hideStatusbar {
if v, err := g.SetView(ct.statusbarviewname, 0, maxY-statusbarHeight-1, ct.maxtablewidth, maxY); err != nil { if v, err := g.SetView(ct.Views.Statusbar.Name, 0, maxY-statusbarHeight-1, ct.maxTableWidth, maxY); err != nil {
if err != gocui.ErrUnknownView { if err != gocui.ErrUnknownView {
return err return err
} }
ct.statusbarview = v ct.Views.Statusbar.Backing = v
ct.statusbarview.Frame = false ct.Views.Statusbar.Backing.Frame = false
ct.colorscheme.SetViewColor(ct.statusbarview, "statusbar") ct.colorscheme.SetViewColor(ct.Views.Statusbar.Backing, "statusbar")
go ct.updateStatusbar("") go ct.updateStatusbar("")
} }
} else { } else {
if ct.statusbarview != nil { if ct.Views.Statusbar.Backing != nil {
if err := g.DeleteView(ct.statusbarviewname); err != nil { if err := g.DeleteView(ct.Views.Statusbar.Name); err != nil {
return err return err
} }
ct.statusbarview = nil ct.Views.Statusbar.Backing = nil
} }
} }
if v, err := g.SetView(ct.searchfieldviewname, 0, maxY-2, ct.maxtablewidth, maxY); err != nil { if v, err := g.SetView(ct.Views.SearchField.Name, 0, maxY-2, ct.maxTableWidth, maxY); err != nil {
if err != gocui.ErrUnknownView { if err != gocui.ErrUnknownView {
return err return err
} }
ct.searchfield = v ct.Views.SearchField.Backing = v
ct.searchfield.Editable = true ct.Views.SearchField.Backing.Editable = true
ct.searchfield.Wrap = true ct.Views.SearchField.Backing.Wrap = true
ct.searchfield.Frame = false ct.Views.SearchField.Backing.Frame = false
ct.colorscheme.SetViewColor(ct.searchfield, "searchbar") ct.colorscheme.SetViewColor(ct.Views.SearchField.Backing, "searchbar")
} }
if v, err := g.SetView(ct.helpviewname, 1, 1, ct.maxtablewidth-1, maxY-1); err != nil { if v, err := g.SetView(ct.Views.Help.Name, 1, 1, ct.maxTableWidth-1, maxY-1); err != nil {
if err != gocui.ErrUnknownView { if err != gocui.ErrUnknownView {
return err return err
} }
ct.helpview = v ct.Views.Help.Backing = v
ct.helpview.Frame = false ct.Views.Help.Backing.Frame = false
ct.colorscheme.SetViewColor(ct.helpview, "menu") ct.colorscheme.SetViewColor(ct.Views.Help.Backing, "menu")
} }
if v, err := g.SetView(ct.portfolioupdatemenuviewname, 1, 1, ct.maxtablewidth-1, maxY-1); err != nil { if v, err := g.SetView(ct.Views.PortfolioUpdateMenu.Name, 1, 1, ct.maxTableWidth-1, maxY-1); err != nil {
if err != gocui.ErrUnknownView { if err != gocui.ErrUnknownView {
return err return err
} }
ct.portfolioupdatemenuview = v ct.Views.PortfolioUpdateMenu.Backing = v
ct.portfolioupdatemenuview.Frame = false ct.Views.PortfolioUpdateMenu.Backing.Frame = false
ct.colorscheme.SetViewColor(ct.portfolioupdatemenuview, "menu") ct.colorscheme.SetViewColor(ct.Views.PortfolioUpdateMenu.Backing, "menu")
} }
if v, err := g.SetView(ct.inputviewname, 3, 6, 30, 8); err != nil { if v, err := g.SetView(ct.Views.Input.Name, 3, 6, 30, 8); err != nil {
if err != gocui.ErrUnknownView { if err != gocui.ErrUnknownView {
return err return err
} }
ct.inputview = v ct.Views.Input.Backing = v
ct.inputview.Frame = true ct.Views.Input.Backing.Frame = true
ct.inputview.Editable = true ct.Views.Input.Backing.Editable = true
ct.inputview.Wrap = true ct.Views.Input.Backing.Wrap = true
ct.colorscheme.SetViewColor(ct.inputview, "menu") ct.colorscheme.SetViewColor(ct.Views.Input.Backing, "menu")
} }
if v, err := g.SetView(ct.convertmenuviewname, 1, 1, ct.maxtablewidth-1, maxY-1); err != nil { if v, err := g.SetView(ct.Views.ConvertMenu.Name, 1, 1, ct.maxTableWidth-1, maxY-1); err != nil {
if err != gocui.ErrUnknownView { if err != gocui.ErrUnknownView {
return err return err
} }
ct.convertmenuview = v ct.Views.ConvertMenu.Backing = v
ct.convertmenuview.Frame = false ct.Views.ConvertMenu.Backing.Frame = false
ct.colorscheme.SetViewColor(ct.convertmenuview, "menu") ct.colorscheme.SetViewColor(ct.Views.ConvertMenu.Backing, "menu")
// 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.Views.SearchField.Name) // hide
g.SetViewOnBottom(ct.helpviewname) // hide g.SetViewOnBottom(ct.Views.Help.Name) // hide
g.SetViewOnBottom(ct.convertmenuviewname) // hide g.SetViewOnBottom(ct.Views.ConvertMenu.Name) // hide
g.SetViewOnBottom(ct.portfolioupdatemenuviewname) // hide g.SetViewOnBottom(ct.Views.PortfolioUpdateMenu.Name) // hide
g.SetViewOnBottom(ct.inputviewname) // hide g.SetViewOnBottom(ct.Views.Input.Name) // hide
ct.setActiveView(ct.tableviewname) ct.setActiveView(ct.Views.Table.Name)
ct.intervalFetchData() ct.intervalFetchData()
} }
return nil return nil
} }
func (ct *Cointop) setActiveView(v string) error {
ct.g.SetViewOnTop(v)
ct.g.SetCurrentView(v)
if v == ct.searchfieldviewname {
ct.searchfield.Clear()
ct.searchfield.SetCursor(1, 0)
fmt.Fprintf(ct.searchfield, "%s", "/")
} else if v == ct.tableviewname {
ct.g.SetViewOnTop(ct.statusbarviewname)
}
if v == ct.portfolioupdatemenuviewname {
ct.g.SetViewOnTop(ct.inputviewname)
ct.g.SetCurrentView(ct.inputviewname)
}
return nil
}
func (ct *Cointop) activeViewName() string {
return ct.g.CurrentView().Name()
}
func (ct *Cointop) setViewOnBottom(v string) error {
_, err := ct.g.SetViewOnBottom(v)
return err
}
func (ct *Cointop) intervalFetchData() {
go func() {
for {
select {
case <-ct.forcerefresh:
ct.refreshAll()
case <-ct.refreshTicker.C:
ct.refreshAll()
}
}
}()
}

@ -14,23 +14,23 @@ var updatecoinsmux sync.Mutex
func (ct *Cointop) updateCoins() error { func (ct *Cointop) updateCoins() error {
coinslock.Lock() coinslock.Lock()
defer coinslock.Unlock() defer coinslock.Unlock()
cachekey := ct.cacheKey("allcoinsslugmap") cachekey := ct.cacheKey("allCoinsSlugMap")
var err error var err error
var allcoinsslugmap map[string]types.Coin var allCoinsSlugMap map[string]types.Coin
cached, found := ct.cache.Get(cachekey) cached, found := ct.cache.Get(cachekey)
_ = cached _ = cached
if found { if found {
// cache hit // cache hit
allcoinsslugmap, _ = cached.(map[string]types.Coin) allCoinsSlugMap, _ = cached.(map[string]types.Coin)
ct.debuglog("soft cache hit") ct.debuglog("soft cache hit")
} }
// cache miss // cache miss
if allcoinsslugmap == nil { if allCoinsSlugMap == nil {
ct.debuglog("cache miss") ct.debuglog("cache miss")
ch := make(chan []types.Coin) ch := make(chan []types.Coin)
err = ct.api.GetAllCoinData(ct.currencyconversion, ch) err = ct.api.GetAllCoinData(ct.State.currencyConversion, ch)
if err != nil { if err != nil {
return err return err
} }
@ -39,7 +39,7 @@ func (ct *Cointop) updateCoins() error {
go ct.processCoins(coins) go ct.processCoins(coins)
} }
} else { } else {
ct.processCoinsMap(allcoinsslugmap) ct.processCoinsMap(allCoinsSlugMap)
} }
return nil return nil
@ -58,14 +58,14 @@ func (ct *Cointop) processCoins(coins []types.Coin) {
updatecoinsmux.Lock() updatecoinsmux.Lock()
defer updatecoinsmux.Unlock() defer updatecoinsmux.Unlock()
cachekey := ct.cacheKey("allcoinsslugmap") cachekey := ct.cacheKey("allCoinsSlugMap")
ct.cache.Set(cachekey, ct.allcoinsslugmap, 10*time.Second) ct.cache.Set(cachekey, ct.State.allCoinsSlugMap, 10*time.Second)
filecache.Set(cachekey, ct.allcoinsslugmap, 24*time.Hour) filecache.Set(cachekey, ct.State.allCoinsSlugMap, 24*time.Hour)
for _, v := range coins { for _, v := range coins {
k := v.Name k := v.Name
last := ct.allcoinsslugmap[k] last := ct.State.allCoinsSlugMap[k]
ct.allcoinsslugmap[k] = &Coin{ ct.State.allCoinsSlugMap[k] = &Coin{
ID: v.ID, ID: v.ID,
Name: v.Name, Name: v.Name,
Symbol: v.Symbol, Symbol: v.Symbol,
@ -81,23 +81,23 @@ func (ct *Cointop) processCoins(coins []types.Coin) {
LastUpdated: v.LastUpdated, LastUpdated: v.LastUpdated,
} }
if last != nil { if last != nil {
ct.allcoinsslugmap[k].Favorite = last.Favorite ct.State.allCoinsSlugMap[k].Favorite = last.Favorite
} }
} }
if len(ct.allcoins) < len(ct.allcoinsslugmap) { if len(ct.State.allCoins) < len(ct.State.allCoinsSlugMap) {
list := []*Coin{} list := []*Coin{}
for _, v := range coins { for _, v := range coins {
k := v.Name k := v.Name
coin := ct.allcoinsslugmap[k] coin := ct.State.allCoinsSlugMap[k]
list = append(list, coin) list = append(list, coin)
} }
ct.allcoins = append(ct.allcoins, list...) ct.State.allCoins = append(ct.State.allCoins, list...)
} else { } else {
// update list in place without changing order // update list in place without changing order
for i := range ct.allcoinsslugmap { for i := range ct.State.allCoinsSlugMap {
cm := ct.allcoinsslugmap[i] cm := ct.State.allCoinsSlugMap[i]
for k := range ct.allcoins { for k := range ct.State.allCoins {
c := ct.allcoins[k] c := ct.State.allCoins[k]
if c.ID == cm.ID { if c.ID == cm.ID {
// TODO: improve this // TODO: improve this
c.ID = cm.ID c.ID = cm.ID
@ -120,7 +120,7 @@ func (ct *Cointop) processCoins(coins []types.Coin) {
} }
time.AfterFunc(10*time.Millisecond, func() { time.AfterFunc(10*time.Millisecond, func() {
ct.sort(ct.sortby, ct.sortdesc, ct.coins, true) ct.sort(ct.State.sortBy, ct.State.sortDesc, ct.State.coins, true)
ct.updateTable() ct.updateTable()
}) })
} }

@ -13,26 +13,26 @@ import (
) )
func (ct *Cointop) updateMarketbar() error { func (ct *Cointop) updateMarketbar() error {
if ct.marketbarview == nil { if ct.Views.Marketbar.Backing == nil {
return nil return nil
} }
maxX := ct.width() maxX := ct.width()
logo := "cointop" logo := "cointop"
if ct.colorschemename == "cointop" { if ct.colorschemeName == "cointop" {
logo = fmt.Sprintf("%s%s%s%s", color.Green(""), color.Cyan(""), color.Green(""), color.Cyan("cointop")) logo = fmt.Sprintf("%s%s%s%s", color.Green(""), color.Cyan(""), color.Green(""), color.Cyan("cointop"))
} }
var content string var content string
if ct.portfoliovisible { if ct.State.portfolioVisible {
total := ct.getPortfolioTotal() total := ct.getPortfolioTotal()
totalstr := humanize.Commaf(total) totalstr := humanize.Commaf(total)
if !(ct.currencyconversion == "BTC" || ct.currencyconversion == "ETH" || total < 1) { if !(ct.State.currencyConversion == "BTC" || ct.State.currencyConversion == "ETH" || total < 1) {
total = math.Round(total*1e2) / 1e2 total = math.Round(total*1e2) / 1e2
totalstr = humanize.Commaf2(total) totalstr = humanize.Commaf2(total)
} }
timeframe := ct.selectedchartrange timeframe := ct.State.selectedChartRange
chartname := ct.selectedCoinName() chartname := ct.selectedCoinName()
var charttitle string var charttitle string
if chartname == "" { if chartname == "" {
@ -63,7 +63,7 @@ func (ct *Cointop) updateMarketbar() error {
} }
chartInfo := "" chartInfo := ""
if !ct.hideChart { if !ct.State.hideChart {
chartInfo = fmt.Sprintf( chartInfo = fmt.Sprintf(
"[ Chart: %s %s ] ", "[ Chart: %s %s ] ",
charttitle, charttitle,
@ -93,7 +93,7 @@ func (ct *Cointop) updateMarketbar() error {
} }
if market.TotalMarketCapUSD == 0 { if market.TotalMarketCapUSD == 0 {
market, err = ct.api.GetGlobalMarketData(ct.currencyconversion) market, err = ct.api.GetGlobalMarketData(ct.State.currencyConversion)
if err != nil { if err != nil {
filecache.Get(cachekey, &market) filecache.Get(cachekey, &market)
} }
@ -104,14 +104,14 @@ func (ct *Cointop) updateMarketbar() error {
}() }()
} }
timeframe := ct.selectedchartrange timeframe := ct.State.selectedChartRange
chartname := ct.selectedCoinName() chartname := ct.selectedCoinName()
if chartname == "" { if chartname == "" {
chartname = "Global" chartname = "Global"
} }
chartInfo := "" chartInfo := ""
if !ct.hideChart { if !ct.State.hideChart {
chartInfo = fmt.Sprintf( chartInfo = fmt.Sprintf(
"[ Chart: %s %s ] ", "[ Chart: %s %s ] ",
ct.colorscheme.MarketBarLabelActive(chartname), ct.colorscheme.MarketBarLabelActive(chartname),
@ -133,12 +133,12 @@ func (ct *Cointop) updateMarketbar() error {
content = ct.colorscheme.Marketbar(content) content = ct.colorscheme.Marketbar(content)
ct.update(func() { ct.update(func() {
if ct.marketbarview == nil { if ct.Views.Marketbar.Backing == nil {
return return
} }
ct.marketbarview.Clear() ct.Views.Marketbar.Backing.Clear()
fmt.Fprintln(ct.marketbarview, content) fmt.Fprintln(ct.Views.Marketbar.Backing, content)
}) })
return nil return nil

@ -1,15 +1,15 @@
package cointop package cointop
func (ct *Cointop) currentPage() int { func (ct *Cointop) currentPage() int {
return ct.page + 1 return ct.State.page + 1
} }
func (ct *Cointop) currentDisplayPage() int { func (ct *Cointop) currentDisplayPage() int {
return ct.page + 1 return ct.State.page + 1
} }
func (ct *Cointop) totalPages() int { func (ct *Cointop) totalPages() int {
return ct.getListCount() / ct.perpage return ct.getListCount() / ct.State.perPage
} }
func (ct *Cointop) totalPagesDisplay() int { func (ct *Cointop) totalPagesDisplay() int {
@ -17,48 +17,30 @@ func (ct *Cointop) totalPagesDisplay() int {
} }
func (ct *Cointop) totalPerPage() int { func (ct *Cointop) totalPerPage() int {
return ct.perpage return ct.State.perPage
} }
func (ct *Cointop) setPage(page int) int { func (ct *Cointop) setPage(page int) int {
if (page*ct.perpage) < ct.getListCount() && page >= 0 { if (page*ct.State.perPage) < ct.getListCount() && page >= 0 {
ct.page = page ct.State.page = page
} }
return ct.page return ct.State.page
}
func (ct *Cointop) highlightRow(idx int) error {
ct.tableview.SetOrigin(0, 0)
ct.tableview.SetCursor(0, 0)
ox, _ := ct.tableview.Origin()
cx, _ := ct.tableview.Cursor()
_, sy := ct.tableview.Size()
perpage := ct.totalPerPage()
p := idx % perpage
oy := (p / sy) * sy
cy := p % sy
if oy > 0 {
ct.tableview.SetOrigin(ox, oy)
}
ct.tableview.SetCursor(cx, cy)
return nil
} }
func (ct *Cointop) cursorDown() error { func (ct *Cointop) cursorDown() error {
if ct.tableview == nil { if ct.Views.Table.Backing == nil {
return nil return nil
} }
_, y := ct.tableview.Origin() _, y := ct.Views.Table.Backing.Origin()
cx, cy := ct.tableview.Cursor() cx, cy := ct.Views.Table.Backing.Cursor()
numRows := len(ct.coins) - 1 numRows := len(ct.State.coins) - 1
if (cy + y + 1) > numRows { if (cy + y + 1) > numRows {
return nil return nil
} }
if err := ct.tableview.SetCursor(cx, cy+1); err != nil { if err := ct.Views.Table.Backing.SetCursor(cx, cy+1); err != nil {
ox, oy := ct.tableview.Origin() ox, oy := ct.Views.Table.Backing.Origin()
// set origin scrolls // set origin scrolls
if err := ct.tableview.SetOrigin(ox, oy+1); err != nil { if err := ct.Views.Table.Backing.SetOrigin(ox, oy+1); err != nil {
return err return err
} }
} }
@ -67,14 +49,14 @@ func (ct *Cointop) cursorDown() error {
} }
func (ct *Cointop) cursorUp() error { func (ct *Cointop) cursorUp() error {
if ct.tableview == nil { if ct.Views.Table.Backing == nil {
return nil return nil
} }
ox, oy := ct.tableview.Origin() ox, oy := ct.Views.Table.Backing.Origin()
cx, cy := ct.tableview.Cursor() cx, cy := ct.Views.Table.Backing.Cursor()
if err := ct.tableview.SetCursor(cx, cy-1); err != nil && oy > 0 { if err := ct.Views.Table.Backing.SetCursor(cx, cy-1); err != nil && oy > 0 {
// set origin scrolls // set origin scrolls
if err := ct.tableview.SetOrigin(ox, oy-1); err != nil { if err := ct.Views.Table.Backing.SetOrigin(ox, oy-1); err != nil {
return err return err
} }
} }
@ -83,14 +65,14 @@ func (ct *Cointop) cursorUp() error {
} }
func (ct *Cointop) pageDown() error { func (ct *Cointop) pageDown() error {
if ct.tableview == nil { if ct.Views.Table.Backing == nil {
return nil return nil
} }
ox, oy := ct.tableview.Origin() // this is prev origin position ox, oy := ct.Views.Table.Backing.Origin() // this is prev origin position
cx, _ := ct.tableview.Cursor() // relative cursor position cx, _ := ct.Views.Table.Backing.Cursor() // relative cursor position
_, sy := ct.tableview.Size() // rows in visible view _, sy := ct.Views.Table.Backing.Size() // rows in visible view
k := oy + sy k := oy + sy
l := len(ct.coins) l := len(ct.State.coins)
// end of table // end of table
if (oy + sy + sy) > l { if (oy + sy + sy) > l {
k = l - sy k = l - sy
@ -101,12 +83,12 @@ func (ct *Cointop) pageDown() error {
sy = l sy = l
} }
if err := ct.tableview.SetOrigin(ox, k); err != nil { if err := ct.Views.Table.Backing.SetOrigin(ox, k); err != nil {
return err return err
} }
// move cursor to last line if can't scroll further // move cursor to last line if can't scroll further
if k == oy { if k == oy {
if err := ct.tableview.SetCursor(cx, sy-1); err != nil { if err := ct.Views.Table.Backing.SetCursor(cx, sy-1); err != nil {
return err return err
} }
} }
@ -115,22 +97,22 @@ func (ct *Cointop) pageDown() error {
} }
func (ct *Cointop) pageUp() error { func (ct *Cointop) pageUp() error {
if ct.tableview == nil { if ct.Views.Table.Backing == nil {
return nil return nil
} }
ox, oy := ct.tableview.Origin() ox, oy := ct.Views.Table.Backing.Origin()
cx, _ := ct.tableview.Cursor() // relative cursor position cx, _ := ct.Views.Table.Backing.Cursor() // relative cursor position
_, sy := ct.tableview.Size() // rows in visible view _, sy := ct.Views.Table.Backing.Size() // rows in visible view
k := oy - sy k := oy - sy
if k < 0 { if k < 0 {
k = 0 k = 0
} }
if err := ct.tableview.SetOrigin(ox, k); err != nil { if err := ct.Views.Table.Backing.SetOrigin(ox, k); err != nil {
return err return err
} }
// move cursor to first line if can't scroll further // move cursor to first line if can't scroll further
if k == oy { if k == oy {
if err := ct.tableview.SetCursor(cx, 0); err != nil { if err := ct.Views.Table.Backing.SetCursor(cx, 0); err != nil {
return err return err
} }
} }
@ -139,15 +121,15 @@ func (ct *Cointop) pageUp() error {
} }
func (ct *Cointop) navigateFirstLine() error { func (ct *Cointop) navigateFirstLine() error {
if ct.tableview == nil { if ct.Views.Table.Backing == nil {
return nil return nil
} }
ox, _ := ct.tableview.Origin() ox, _ := ct.Views.Table.Backing.Origin()
cx, _ := ct.tableview.Cursor() cx, _ := ct.Views.Table.Backing.Cursor()
if err := ct.tableview.SetOrigin(ox, 0); err != nil { if err := ct.Views.Table.Backing.SetOrigin(ox, 0); err != nil {
return err return err
} }
if err := ct.tableview.SetCursor(cx, 0); err != nil { if err := ct.Views.Table.Backing.SetCursor(cx, 0); err != nil {
return err return err
} }
ct.rowChanged() ct.rowChanged()
@ -155,18 +137,18 @@ func (ct *Cointop) navigateFirstLine() error {
} }
func (ct *Cointop) navigateLastLine() error { func (ct *Cointop) navigateLastLine() error {
if ct.tableview == nil { if ct.Views.Table.Backing == nil {
return nil return nil
} }
ox, _ := ct.tableview.Origin() ox, _ := ct.Views.Table.Backing.Origin()
cx, _ := ct.tableview.Cursor() cx, _ := ct.Views.Table.Backing.Cursor()
_, sy := ct.tableview.Size() _, sy := ct.Views.Table.Backing.Size()
l := len(ct.coins) l := len(ct.State.coins)
k := l - sy k := l - sy
if err := ct.tableview.SetOrigin(ox, k); err != nil { if err := ct.Views.Table.Backing.SetOrigin(ox, k); err != nil {
return err return err
} }
if err := ct.tableview.SetCursor(cx, sy-1); err != nil { if err := ct.Views.Table.Backing.SetCursor(cx, sy-1); err != nil {
return err return err
} }
ct.rowChanged() ct.rowChanged()
@ -174,11 +156,11 @@ func (ct *Cointop) navigateLastLine() error {
} }
func (ct *Cointop) navigatePageFirstLine() error { func (ct *Cointop) navigatePageFirstLine() error {
if ct.tableview == nil { if ct.Views.Table.Backing == nil {
return nil return nil
} }
cx, _ := ct.tableview.Cursor() cx, _ := ct.Views.Table.Backing.Cursor()
if err := ct.tableview.SetCursor(cx, 0); err != nil { if err := ct.Views.Table.Backing.SetCursor(cx, 0); err != nil {
return err return err
} }
ct.rowChanged() ct.rowChanged()
@ -186,12 +168,12 @@ func (ct *Cointop) navigatePageFirstLine() error {
} }
func (ct *Cointop) navigatePageMiddleLine() error { func (ct *Cointop) navigatePageMiddleLine() error {
if ct.tableview == nil { if ct.Views.Table.Backing == nil {
return nil return nil
} }
cx, _ := ct.tableview.Cursor() cx, _ := ct.Views.Table.Backing.Cursor()
_, sy := ct.tableview.Size() _, sy := ct.Views.Table.Backing.Size()
if err := ct.tableview.SetCursor(cx, (sy/2)-1); err != nil { if err := ct.Views.Table.Backing.SetCursor(cx, (sy/2)-1); err != nil {
return err return err
} }
ct.rowChanged() ct.rowChanged()
@ -199,12 +181,12 @@ func (ct *Cointop) navigatePageMiddleLine() error {
} }
func (ct *Cointop) navigatePageLastLine() error { func (ct *Cointop) navigatePageLastLine() error {
if ct.tableview == nil { if ct.Views.Table.Backing == nil {
return nil return nil
} }
cx, _ := ct.tableview.Cursor() cx, _ := ct.Views.Table.Backing.Cursor()
_, sy := ct.tableview.Size() _, sy := ct.Views.Table.Backing.Size()
if err := ct.tableview.SetCursor(cx, sy-1); err != nil { if err := ct.Views.Table.Backing.SetCursor(cx, sy-1); err != nil {
return err return err
} }
ct.rowChanged() ct.rowChanged()
@ -215,32 +197,32 @@ func (ct *Cointop) prevPage() error {
if ct.isFirstPage() { if ct.isFirstPage() {
return nil return nil
} }
ct.setPage(ct.page - 1) ct.setPage(ct.State.page - 1)
ct.updateTable() ct.updateTable()
ct.rowChanged() ct.rowChanged()
return nil return nil
} }
func (ct *Cointop) nextPage() error { func (ct *Cointop) nextPage() error {
ct.setPage(ct.page + 1) ct.setPage(ct.State.page + 1)
ct.updateTable() ct.updateTable()
ct.rowChanged() ct.rowChanged()
return nil return nil
} }
func (ct *Cointop) firstPage() error { func (ct *Cointop) firstPage() error {
ct.page = 0 ct.State.page = 0
ct.updateTable() ct.updateTable()
ct.rowChanged() ct.rowChanged()
return nil return nil
} }
func (ct *Cointop) isFirstPage() bool { func (ct *Cointop) isFirstPage() bool {
return ct.page == 0 return ct.State.page == 0
} }
func (ct *Cointop) lastPage() error { func (ct *Cointop) lastPage() error {
ct.page = ct.getListCount() / ct.perpage ct.State.page = ct.getListCount() / ct.State.perPage
ct.updateTable() ct.updateTable()
ct.rowChanged() ct.rowChanged()
return nil return nil
@ -255,3 +237,21 @@ func (ct *Cointop) goToGlobalIndex(idx int) error {
ct.updateTable() ct.updateTable()
return nil return nil
} }
func (ct *Cointop) highlightRow(idx int) error {
ct.Views.Table.Backing.SetOrigin(0, 0)
ct.Views.Table.Backing.SetCursor(0, 0)
ox, _ := ct.Views.Table.Backing.Origin()
cx, _ := ct.Views.Table.Backing.Cursor()
_, sy := ct.Views.Table.Backing.Size()
perpage := ct.totalPerPage()
p := idx % perpage
oy := (p / sy) * sy
cy := p % sy
if oy > 0 {
ct.Views.Table.Backing.SetOrigin(ox, oy)
}
ct.Views.Table.Backing.SetCursor(cx, cy)
return nil
}

@ -11,22 +11,22 @@ import (
) )
func (ct *Cointop) togglePortfolio() error { func (ct *Cointop) togglePortfolio() error {
ct.filterByFavorites = false ct.State.filterByFavorites = false
ct.portfoliovisible = !ct.portfoliovisible ct.State.portfolioVisible = !ct.State.portfolioVisible
go ct.updateTable() go ct.updateTable()
return nil return nil
} }
func (ct *Cointop) toggleShowPortfolio() error { func (ct *Cointop) toggleShowPortfolio() error {
ct.filterByFavorites = false ct.State.filterByFavorites = false
ct.portfoliovisible = true ct.State.portfolioVisible = true
go ct.updateTable() go ct.updateTable()
return nil return nil
} }
func (ct *Cointop) togglePortfolioUpdateMenu() error { func (ct *Cointop) togglePortfolioUpdateMenu() error {
ct.portfolioupdatemenuvisible = !ct.portfolioupdatemenuvisible ct.State.portfolioUpdateMenuVisible = !ct.State.portfolioUpdateMenuVisible
if ct.portfolioupdatemenuvisible { if ct.State.portfolioUpdateMenuVisible {
return ct.showPortfolioUpdateMenu() return ct.showPortfolioUpdateMenu()
} }
return ct.hidePortfolioUpdateMenu() return ct.hidePortfolioUpdateMenu()
@ -34,7 +34,7 @@ func (ct *Cointop) togglePortfolioUpdateMenu() error {
func (ct *Cointop) updatePortfolioUpdateMenu() { func (ct *Cointop) updatePortfolioUpdateMenu() {
coin := ct.highlightedRowCoin() coin := ct.highlightedRowCoin()
exists := ct.portfolioEntryExists(coin) exists := ct.PortfolioEntryExists(coin)
value := strconv.FormatFloat(coin.Holdings, 'f', -1, 64) value := strconv.FormatFloat(coin.Holdings, 'f', -1, 64)
var mode string var mode string
var current string var current string
@ -47,16 +47,16 @@ func (ct *Cointop) updatePortfolioUpdateMenu() {
mode = "Add" mode = "Add"
submitText = "Add" submitText = "Add"
} }
header := ct.colorscheme.MenuHeader(fmt.Sprintf(" %s Portfolio Entry %s\n\n", mode, pad.Left("[q] close ", ct.maxtablewidth-26, " "))) header := ct.colorscheme.MenuHeader(fmt.Sprintf(" %s Portfolio Entry %s\n\n", mode, pad.Left("[q] close ", ct.maxTableWidth-26, " ")))
label := fmt.Sprintf(" Enter holdings for %s %s", ct.colorscheme.MenuLabel(coin.Name), current) label := fmt.Sprintf(" Enter holdings for %s %s", ct.colorscheme.MenuLabel(coin.Name), current)
content := fmt.Sprintf("%s\n%s\n\n%s%s\n\n\n [Enter] %s [ESC] Cancel", header, label, strings.Repeat(" ", 29), coin.Symbol, submitText) content := fmt.Sprintf("%s\n%s\n\n%s%s\n\n\n [Enter] %s [ESC] Cancel", header, label, strings.Repeat(" ", 29), coin.Symbol, submitText)
ct.update(func() { ct.update(func() {
ct.portfolioupdatemenuview.Clear() ct.Views.PortfolioUpdateMenu.Backing.Clear()
ct.portfolioupdatemenuview.Frame = true ct.Views.PortfolioUpdateMenu.Backing.Frame = true
fmt.Fprintln(ct.portfolioupdatemenuview, content) fmt.Fprintln(ct.Views.PortfolioUpdateMenu.Backing, content)
fmt.Fprintln(ct.inputview, value) fmt.Fprintln(ct.Views.Input.Backing, value)
ct.inputview.SetCursor(len(value), 0) ct.Views.Input.Backing.SetCursor(len(value), 0)
}) })
} }
@ -67,28 +67,28 @@ func (ct *Cointop) showPortfolioUpdateMenu() error {
return nil return nil
} }
ct.portfolioupdatemenuvisible = true ct.State.portfolioUpdateMenuVisible = true
ct.updatePortfolioUpdateMenu() ct.updatePortfolioUpdateMenu()
ct.setActiveView(ct.portfolioupdatemenuviewname) ct.setActiveView(ct.Views.PortfolioUpdateMenu.Name)
return nil return nil
} }
func (ct *Cointop) hidePortfolioUpdateMenu() error { func (ct *Cointop) hidePortfolioUpdateMenu() error {
ct.portfolioupdatemenuvisible = false ct.State.portfolioUpdateMenuVisible = false
ct.setViewOnBottom(ct.portfolioupdatemenuviewname) ct.setViewOnBottom(ct.Views.PortfolioUpdateMenu.Name)
ct.setViewOnBottom(ct.inputviewname) ct.setViewOnBottom(ct.Views.Input.Name)
ct.setActiveView(ct.tableviewname) ct.setActiveView(ct.Views.Table.Name)
ct.update(func() { ct.update(func() {
if ct.portfolioupdatemenuview == nil { if ct.Views.PortfolioUpdateMenu.Backing == nil {
return return
} }
ct.portfolioupdatemenuview.Clear() ct.Views.PortfolioUpdateMenu.Backing.Clear()
ct.portfolioupdatemenuview.Frame = false ct.Views.PortfolioUpdateMenu.Backing.Frame = false
fmt.Fprintln(ct.portfolioupdatemenuview, "") fmt.Fprintln(ct.Views.PortfolioUpdateMenu.Backing, "")
ct.inputview.Clear() ct.Views.Input.Backing.Clear()
fmt.Fprintln(ct.inputview, "") fmt.Fprintln(ct.Views.Input.Backing, "")
}) })
return nil return nil
} }
@ -99,7 +99,7 @@ func (ct *Cointop) setPortfolioHoldings() error {
coin := ct.highlightedRowCoin() coin := ct.highlightedRowCoin()
b := make([]byte, 100) b := make([]byte, 100)
n, err := ct.inputview.Read(b) n, err := ct.Views.Input.Backing.Read(b)
if n == 0 { if n == 0 {
return nil return nil
} }
@ -129,20 +129,20 @@ func (ct *Cointop) setPortfolioHoldings() error {
return nil return nil
} }
func (ct *Cointop) portfolioEntry(c *Coin) (*portfolioEntry, bool) { func (ct *Cointop) PortfolioEntry(c *Coin) (*PortfolioEntry, bool) {
if c == nil { if c == nil {
return &portfolioEntry{}, true return &PortfolioEntry{}, true
} }
var p *portfolioEntry var p *PortfolioEntry
var isNew bool var isNew bool
var ok bool var ok bool
key := strings.ToLower(c.Name) key := strings.ToLower(c.Name)
if p, ok = ct.portfolio.Entries[key]; !ok { if p, ok = ct.State.portfolio.Entries[key]; !ok {
// NOTE: if not found then try the symbol // NOTE: if not found then try the symbol
key := strings.ToLower(c.Symbol) key := strings.ToLower(c.Symbol)
if p, ok = ct.portfolio.Entries[key]; !ok { if p, ok = ct.State.portfolio.Entries[key]; !ok {
p = &portfolioEntry{ p = &PortfolioEntry{
Coin: c.Name, Coin: c.Name,
Holdings: 0, Holdings: 0,
} }
@ -154,11 +154,11 @@ func (ct *Cointop) portfolioEntry(c *Coin) (*portfolioEntry, bool) {
} }
func (ct *Cointop) setPortfolioEntry(coin string, holdings float64) { func (ct *Cointop) setPortfolioEntry(coin string, holdings float64) {
c, _ := ct.allcoinsslugmap[strings.ToLower(coin)] c, _ := ct.State.allCoinsSlugMap[strings.ToLower(coin)]
p, isNew := ct.portfolioEntry(c) p, isNew := ct.PortfolioEntry(c)
if isNew { if isNew {
key := strings.ToLower(coin) key := strings.ToLower(coin)
ct.portfolio.Entries[key] = &portfolioEntry{ ct.State.portfolio.Entries[key] = &PortfolioEntry{
Coin: coin, Coin: coin,
Holdings: holdings, Holdings: holdings,
} }
@ -168,33 +168,33 @@ func (ct *Cointop) setPortfolioEntry(coin string, holdings float64) {
} }
func (ct *Cointop) removePortfolioEntry(coin string) { func (ct *Cointop) removePortfolioEntry(coin string) {
delete(ct.portfolio.Entries, strings.ToLower(coin)) delete(ct.State.portfolio.Entries, strings.ToLower(coin))
} }
func (ct *Cointop) portfolioEntryExists(c *Coin) bool { func (ct *Cointop) PortfolioEntryExists(c *Coin) bool {
_, isNew := ct.portfolioEntry(c) _, isNew := ct.PortfolioEntry(c)
return !isNew return !isNew
} }
func (ct *Cointop) portfolioEntriesCount() int { func (ct *Cointop) portfolioEntriesCount() int {
return len(ct.portfolio.Entries) return len(ct.State.portfolio.Entries)
} }
func (ct *Cointop) getPortfolioSlice() []*Coin { func (ct *Cointop) getPortfolioSlice() []*Coin {
sliced := []*Coin{} sliced := []*Coin{}
for i := range ct.allcoins { for i := range ct.State.allCoins {
if ct.portfolioEntriesCount() == 0 { if ct.portfolioEntriesCount() == 0 {
break break
} }
coin := ct.allcoins[i] coin := ct.State.allCoins[i]
p, isNew := ct.portfolioEntry(coin) p, isNew := ct.PortfolioEntry(coin)
if isNew { if isNew {
continue continue
} }
coin.Holdings = p.Holdings coin.Holdings = p.Holdings
balance := coin.Price * p.Holdings balance := coin.Price * p.Holdings
balancestr := fmt.Sprintf("%.2f", balance) balancestr := fmt.Sprintf("%.2f", balance)
if ct.currencyconversion == "ETH" || ct.currencyconversion == "BTC" { if ct.State.currencyConversion == "ETH" || ct.State.currencyConversion == "BTC" {
balancestr = fmt.Sprintf("%.5f", balance) balancestr = fmt.Sprintf("%.5f", balance)
} }
balance, _ = strconv.ParseFloat(balancestr, 64) balance, _ = strconv.ParseFloat(balancestr, 64)

@ -11,15 +11,15 @@ func (ct *Cointop) quit() error {
} }
func (ct *Cointop) quitView() error { func (ct *Cointop) quitView() error {
if ct.portfoliovisible { if ct.State.portfolioVisible {
ct.portfoliovisible = false ct.State.portfolioVisible = false
return ct.updateTable() return ct.updateTable()
} }
if ct.filterByFavorites { if ct.State.filterByFavorites {
ct.filterByFavorites = false ct.State.filterByFavorites = false
return ct.updateTable() return ct.updateTable()
} }
if ct.activeViewName() == ct.tableviewname { if ct.activeViewName() == ct.Views.Table.Name {
return ct.quit() return ct.quit()
} }

@ -8,16 +8,16 @@ import (
func (ct *Cointop) refresh() error { func (ct *Cointop) refresh() error {
go func() { go func() {
<-ct.limiter <-ct.limiter
ct.forcerefresh <- true ct.forceRefresh <- true
}() }()
return nil return nil
} }
func (ct *Cointop) refreshAll() error { func (ct *Cointop) refreshAll() error {
ct.refreshmux.Lock() ct.refreshMux.Lock()
defer ct.refreshmux.Unlock() defer ct.refreshMux.Unlock()
ct.setRefreshStatus() ct.setRefreshStatus()
ct.cache.Delete("allcoinsslugmap") ct.cache.Delete("allCoinsSlugMap")
ct.cache.Delete("market") ct.cache.Delete("market")
go func() { go func() {
ct.updateCoins() ct.updateCoins()
@ -46,3 +46,16 @@ func (ct *Cointop) loadingTicks(s string, t int) {
} }
} }
} }
func (ct *Cointop) intervalFetchData() {
go func() {
for {
select {
case <-ct.forceRefresh:
ct.refreshAll()
case <-ct.refreshTicker.C:
ct.refreshAll()
}
}
}()
}

@ -8,27 +8,27 @@ import (
) )
func (ct *Cointop) openSearch() error { func (ct *Cointop) openSearch() error {
ct.searchfieldvisible = true ct.State.searchFieldVisible = true
ct.setActiveView(ct.searchfieldviewname) ct.setActiveView(ct.Views.SearchField.Name)
return nil return nil
} }
func (ct *Cointop) cancelSearch() error { func (ct *Cointop) cancelSearch() error {
ct.searchfieldvisible = false ct.State.searchFieldVisible = false
ct.setActiveView(ct.tableviewname) ct.setActiveView(ct.Views.Table.Name)
return nil return nil
} }
func (ct *Cointop) doSearch() error { func (ct *Cointop) doSearch() error {
ct.searchfield.Rewind() ct.Views.SearchField.Backing.Rewind()
b := make([]byte, 100) b := make([]byte, 100)
n, err := ct.searchfield.Read(b) n, err := ct.Views.SearchField.Backing.Read(b)
// TODO: do this a better way (SoC) // TODO: do this a better way (SoC)
ct.filterByFavorites = false ct.State.filterByFavorites = false
ct.portfoliovisible = false ct.State.portfolioVisible = false
defer ct.setActiveView(ct.tableviewname) defer ct.setActiveView(ct.Views.Table.Name)
if err != nil { if err != nil {
return nil return nil
} }
@ -51,8 +51,8 @@ func (ct *Cointop) search(q string) error {
min := -1 min := -1
var hasprefixidx []int var hasprefixidx []int
var hasprefixdist []int var hasprefixdist []int
for i := range ct.allcoins { for i := range ct.State.allCoins {
coin := ct.allcoins[i] coin := ct.State.allCoins[i]
name := strings.ToLower(coin.Name) name := strings.ToLower(coin.Name)
symbol := strings.ToLower(coin.Symbol) symbol := strings.ToLower(coin.Symbol)
// if query matches symbol, return immediately // if query matches symbol, return immediately

@ -0,0 +1,19 @@
package cointop
func (ct *Cointop) selectedCoinName() string {
coin := ct.State.selectedCoin
if coin != nil {
return coin.Name
}
return ""
}
func (ct *Cointop) selectedCoinSymbol() string {
coin := ct.State.selectedCoin
if coin != nil {
return coin.Symbol
}
return ""
}

@ -9,7 +9,7 @@ import (
var sortlock sync.Mutex var sortlock sync.Mutex
func (ct *Cointop) sort(sortby string, desc bool, list []*Coin, renderHeaders bool) { func (ct *Cointop) sort(sortBy string, desc bool, list []*Coin, renderHeaders bool) {
sortlock.Lock() sortlock.Lock()
defer sortlock.Unlock() defer sortlock.Unlock()
if list == nil { if list == nil {
@ -18,10 +18,10 @@ func (ct *Cointop) sort(sortby string, desc bool, list []*Coin, renderHeaders bo
if len(list) < 2 { if len(list) < 2 {
return return
} }
ct.sortby = sortby ct.State.sortBy = sortBy
ct.sortdesc = desc ct.State.sortDesc = desc
sort.Slice(list[:], func(i, j int) bool { sort.Slice(list[:], func(i, j int) bool {
if ct.sortdesc { if ct.State.sortDesc {
i, j = j, i i, j = j, i
} }
a := list[i] a := list[i]
@ -32,7 +32,7 @@ func (ct *Cointop) sort(sortby string, desc bool, list []*Coin, renderHeaders bo
if b == nil { if b == nil {
return false return false
} }
switch sortby { switch sortBy {
case "rank": case "rank":
return a.Rank < b.Rank return a.Rank < b.Rank
case "name": case "name":
@ -71,66 +71,66 @@ func (ct *Cointop) sort(sortby string, desc bool, list []*Coin, renderHeaders bo
} }
} }
func (ct *Cointop) sortToggle(sortby string, desc bool) error {
if ct.sortby == sortby {
desc = !ct.sortdesc
}
ct.sort(sortby, desc, ct.coins, true)
ct.updateTable()
return nil
}
func (ct *Cointop) sortfn(sortby string, desc bool) func(g *gocui.Gui, v *gocui.View) error {
return func(g *gocui.Gui, v *gocui.View) error {
return ct.sortToggle(sortby, desc)
}
}
func (ct *Cointop) getSortColIndex() int {
for i, col := range ct.tablecolumnorder {
if ct.sortby == col {
return i
}
}
return 0
}
func (ct *Cointop) sortAsc() error { func (ct *Cointop) sortAsc() error {
ct.sortdesc = false ct.State.sortDesc = false
ct.updateTable() ct.updateTable()
return nil return nil
} }
func (ct *Cointop) sortDesc() error { func (ct *Cointop) sortDesc() error {
ct.sortdesc = true ct.State.sortDesc = true
ct.updateTable() ct.updateTable()
return nil return nil
} }
func (ct *Cointop) sortPrevCol() error { func (ct *Cointop) sortPrevCol() error {
nextsortby := ct.tablecolumnorder[0] nextsortBy := ct.tableColumnOrder[0]
i := ct.getSortColIndex() i := ct.getSortColIndex()
k := i - 1 k := i - 1
if k < 0 { if k < 0 {
k = 0 k = 0
} }
nextsortby = ct.tablecolumnorder[k] nextsortBy = ct.tableColumnOrder[k]
ct.sort(nextsortby, ct.sortdesc, ct.coins, true) ct.sort(nextsortBy, ct.State.sortDesc, ct.State.coins, true)
ct.updateTable() ct.updateTable()
return nil return nil
} }
func (ct *Cointop) sortNextCol() error { func (ct *Cointop) sortNextCol() error {
nextsortby := ct.tablecolumnorder[0] nextsortBy := ct.tableColumnOrder[0]
l := len(ct.tablecolumnorder) l := len(ct.tableColumnOrder)
i := ct.getSortColIndex() i := ct.getSortColIndex()
k := i + 1 k := i + 1
if k > l-1 { if k > l-1 {
k = l - 1 k = l - 1
} }
nextsortby = ct.tablecolumnorder[k] nextsortBy = ct.tableColumnOrder[k]
ct.sort(nextsortby, ct.sortdesc, ct.coins, true) ct.sort(nextsortBy, ct.State.sortDesc, ct.State.coins, true)
ct.updateTable() ct.updateTable()
return nil return nil
} }
func (ct *Cointop) sortToggle(sortBy string, desc bool) error {
if ct.State.sortBy == sortBy {
desc = !ct.State.sortDesc
}
ct.sort(sortBy, desc, ct.State.coins, true)
ct.updateTable()
return nil
}
func (ct *Cointop) sortfn(sortBy string, desc bool) func(g *gocui.Gui, v *gocui.View) error {
return func(g *gocui.Gui, v *gocui.View) error {
return ct.sortToggle(sortBy, desc)
}
}
func (ct *Cointop) getSortColIndex() int {
for i, col := range ct.tableColumnOrder {
if ct.State.sortBy == col {
return i
}
}
return 0
}

@ -8,7 +8,7 @@ import (
) )
func (ct *Cointop) updateStatusbar(s string) error { func (ct *Cointop) updateStatusbar(s string) error {
if ct.statusbarview == nil { if ct.Views.Statusbar.Backing == nil {
return nil return nil
} }
@ -17,33 +17,33 @@ func (ct *Cointop) updateStatusbar(s string) error {
var quitText string var quitText string
var favoritesText string var favoritesText string
var portfolioText string var portfolioText string
if ct.portfoliovisible || ct.filterByFavorites { if ct.State.portfolioVisible || ct.State.filterByFavorites {
quitText = "Return" quitText = "Return"
} else { } else {
quitText = "Quit" quitText = "Quit"
} }
if ct.portfoliovisible { if ct.State.portfolioVisible {
portfolioText = "[E]Edit" portfolioText = "[E]Edit"
} else { } else {
portfolioText = "[P]Portfolio" portfolioText = "[P]Portfolio"
} }
if ct.filterByFavorites { if ct.State.filterByFavorites {
favoritesText = "[Space]Unfavorite" favoritesText = "[Space]Unfavorite"
} else { } else {
favoritesText = "[F]Favorites" favoritesText = "[F]Favorites"
} }
ct.update(func() { ct.update(func() {
if ct.statusbarview == nil { if ct.Views.Statusbar.Backing == nil {
return return
} }
ct.statusbarview.Clear() ct.Views.Statusbar.Backing.Clear()
base := fmt.Sprintf("%s%s %sHelp %sChart %sRange %sSearch %sConvert %s %s %sSave", "[Q]", quitText, "[?]", "[Enter]", "[[ ]]", "[/]", "[C]", favoritesText, portfolioText, "[CTRL-S]") base := fmt.Sprintf("%s%s %sHelp %sChart %sRange %sSearch %sConvert %s %s %sSave", "[Q]", quitText, "[?]", "[Enter]", "[[ ]]", "[/]", "[C]", favoritesText, portfolioText, "[CTRL-S]")
str := pad.Right(fmt.Sprintf("%v %sPage %v/%v %s", base, "[← →]", currpage, totalpages, s), ct.maxtablewidth, " ") str := pad.Right(fmt.Sprintf("%v %sPage %v/%v %s", base, "[← →]", currpage, totalpages, s), ct.maxTableWidth, " ")
v := fmt.Sprintf("v%s", ct.version()) v := fmt.Sprintf("v%s", ct.version())
str = str[:len(str)-len(v)+2] + v str = str[:len(str)-len(v)+2] + v
fmt.Fprintln(ct.statusbarview, str) fmt.Fprintln(ct.Views.Statusbar.Backing, str)
}) })
return nil return nil

@ -18,7 +18,7 @@ func (ct *Cointop) refreshTable() error {
ct.table = table.New().SetWidth(maxX) ct.table = table.New().SetWidth(maxX)
ct.table.HideColumHeaders = true ct.table.HideColumHeaders = true
if ct.portfoliovisible { if ct.State.portfolioVisible {
ct.table.AddCol("") ct.table.AddCol("")
ct.table.AddCol("") ct.table.AddCol("")
ct.table.AddCol("") ct.table.AddCol("")
@ -31,7 +31,7 @@ func (ct *Cointop) refreshTable() error {
total := ct.getPortfolioTotal() total := ct.getPortfolioTotal()
for _, coin := range ct.coins { for _, coin := range ct.State.coins {
unix, _ := strconv.ParseInt(coin.LastUpdated, 10, 64) unix, _ := strconv.ParseInt(coin.LastUpdated, 10, 64)
lastUpdated := time.Unix(unix, 0).Format("15:04:05 Jan 02") lastUpdated := time.Unix(unix, 0).Format("15:04:05 Jan 02")
colorbalance := ct.colorscheme.TableColumnPrice colorbalance := ct.colorscheme.TableColumnPrice
@ -80,7 +80,7 @@ func (ct *Cointop) refreshTable() error {
ct.table.AddCol("") ct.table.AddCol("")
ct.table.AddCol("") ct.table.AddCol("")
ct.table.AddCol("") ct.table.AddCol("")
for _, coin := range ct.coins { for _, coin := range ct.State.coins {
if coin == nil { if coin == nil {
continue continue
} }
@ -125,7 +125,7 @@ func (ct *Cointop) refreshTable() error {
symbolpadding := 5 symbolpadding := 5
// NOTE: this is to adjust padding by 1 because when all name rows are // NOTE: this is to adjust padding by 1 because when all name rows are
// yellow it messes the spacing (need to debug) // yellow it messes the spacing (need to debug)
if ct.filterByFavorites { if ct.State.filterByFavorites {
symbolpadding = 6 symbolpadding = 6
} }
ct.table.AddRow( ct.table.AddRow(
@ -148,17 +148,17 @@ func (ct *Cointop) refreshTable() error {
// highlight last row if current row is out of bounds (can happen when switching views) // highlight last row if current row is out of bounds (can happen when switching views)
currentrow := ct.highlightedRowIndex() currentrow := ct.highlightedRowIndex()
if len(ct.coins) > currentrow { if len(ct.State.coins) > currentrow {
ct.highlightRow(currentrow) ct.highlightRow(currentrow)
} }
ct.update(func() { ct.update(func() {
if ct.tableview == nil { if ct.Views.Table.Backing == nil {
return return
} }
ct.tableview.Clear() ct.Views.Table.Backing.Clear()
ct.table.Format().Fprint(ct.tableview) ct.table.Format().Fprint(ct.Views.Table.Backing)
go ct.rowChanged() go ct.rowChanged()
go ct.updateHeaders() go ct.updateHeaders()
go ct.updateMarketbar() go ct.updateMarketbar()
@ -171,36 +171,36 @@ func (ct *Cointop) refreshTable() error {
func (ct *Cointop) updateTable() error { func (ct *Cointop) updateTable() error {
sliced := []*Coin{} sliced := []*Coin{}
for i := range ct.allcoinsslugmap { for i := range ct.State.allCoinsSlugMap {
v := ct.allcoinsslugmap[i] v := ct.State.allCoinsSlugMap[i]
if ct.favorites[v.Name] { if ct.State.favorites[v.Name] {
v.Favorite = true v.Favorite = true
} }
} }
if ct.filterByFavorites { if ct.State.filterByFavorites {
for i := range ct.allcoins { for i := range ct.State.allCoins {
coin := ct.allcoins[i] coin := ct.State.allCoins[i]
if coin.Favorite { if coin.Favorite {
sliced = append(sliced, coin) sliced = append(sliced, coin)
} }
} }
ct.coins = sliced ct.State.coins = sliced
go ct.refreshTable() go ct.refreshTable()
return nil return nil
} }
if ct.portfoliovisible { if ct.State.portfolioVisible {
sliced = ct.getPortfolioSlice() sliced = ct.getPortfolioSlice()
ct.coins = sliced ct.State.coins = sliced
go ct.refreshTable() go ct.refreshTable()
return nil return nil
} }
start := ct.page * ct.perpage start := ct.State.page * ct.State.perPage
end := start + ct.perpage end := start + ct.State.perPage
allcoins := ct.allCoins() allCoins := ct.allCoins()
size := len(allcoins) size := len(allCoins)
if start < 0 { if start < 0 {
start = 0 start = 0
} }
@ -221,34 +221,34 @@ func (ct *Cointop) updateTable() error {
return nil return nil
} }
if end > 0 { if end > 0 {
sliced = allcoins[start:end] sliced = allCoins[start:end]
} }
ct.coins = sliced ct.State.coins = sliced
ct.sort(ct.sortby, ct.sortdesc, ct.coins, true) ct.sort(ct.State.sortBy, ct.State.sortDesc, ct.State.coins, true)
go ct.refreshTable() go ct.refreshTable()
return nil return nil
} }
func (ct *Cointop) highlightedRowIndex() int { func (ct *Cointop) highlightedRowIndex() int {
_, y := ct.tableview.Origin() _, y := ct.Views.Table.Backing.Origin()
_, cy := ct.tableview.Cursor() _, cy := ct.Views.Table.Backing.Cursor()
idx := y + cy idx := y + cy
if idx < 0 { if idx < 0 {
idx = 0 idx = 0
} }
if idx >= len(ct.coins) { if idx >= len(ct.State.coins) {
idx = len(ct.coins) - 1 idx = len(ct.State.coins) - 1
} }
return idx return idx
} }
func (ct *Cointop) highlightedRowCoin() *Coin { func (ct *Cointop) highlightedRowCoin() *Coin {
idx := ct.highlightedRowIndex() idx := ct.highlightedRowIndex()
if len(ct.coins) == 0 { if len(ct.State.coins) == 0 {
return nil return nil
} }
return ct.coins[idx] return ct.State.coins[idx]
} }
func (ct *Cointop) rowLink() string { func (ct *Cointop) rowLink() string {
@ -282,46 +282,9 @@ func (ct *Cointop) rowLinkShort() string {
return "" return ""
} }
func (ct *Cointop) allCoins() []*Coin {
if ct.filterByFavorites {
var list []*Coin
for i := range ct.allcoins {
coin := ct.allcoins[i]
if coin.Favorite {
list = append(list, coin)
}
}
return list
}
if ct.portfoliovisible {
var list []*Coin
for i := range ct.allcoins {
coin := ct.allcoins[i]
if ct.portfolioEntryExists(coin) {
list = append(list, coin)
}
}
return list
}
return ct.allcoins
}
func (ct *Cointop) coinBySymbol(symbol string) *Coin {
for i := range ct.allcoins {
coin := ct.allcoins[i]
if coin.Symbol == symbol {
return coin
}
}
return nil
}
func (ct *Cointop) toggleTableFullscreen() error { func (ct *Cointop) toggleTableFullscreen() error {
ct.onlyTable = !ct.onlyTable ct.State.onlyTable = !ct.State.onlyTable
if ct.onlyTable { if ct.State.onlyTable {
} else { } else {
// NOTE: cached values are initial config settings. // NOTE: cached values are initial config settings.
// If the only-table config was set then toggle // If the only-table config was set then toggle
@ -329,17 +292,17 @@ func (ct *Cointop) toggleTableFullscreen() error {
onlyTable, _ := ct.cache.Get("onlyTable") onlyTable, _ := ct.cache.Get("onlyTable")
if onlyTable.(bool) { if onlyTable.(bool) {
ct.hideMarketbar = false ct.State.hideMarketbar = false
ct.hideChart = false ct.State.hideChart = false
ct.hideStatusbar = false ct.State.hideStatusbar = false
} else { } else {
// NOTE: cached values store initial hidden views preferences. // NOTE: cached values store initial hidden views preferences.
hideMarketbar, _ := ct.cache.Get("hideMarketbar") hideMarketbar, _ := ct.cache.Get("hideMarketbar")
ct.hideMarketbar = hideMarketbar.(bool) ct.State.hideMarketbar = hideMarketbar.(bool)
hideChart, _ := ct.cache.Get("hideChart") hideChart, _ := ct.cache.Get("hideChart")
ct.hideChart = hideChart.(bool) ct.State.hideChart = hideChart.(bool)
hideStatusbar, _ := ct.cache.Get("hideStatusbar") hideStatusbar, _ := ct.cache.Get("hideStatusbar")
ct.hideStatusbar = hideStatusbar.(bool) ct.State.hideStatusbar = hideStatusbar.(bool)
} }
} }

@ -37,9 +37,9 @@ func (ct *Cointop) updateHeaders() {
for k := range cm { for k := range cm {
cm[k].arrow = " " cm[k].arrow = " "
if ct.sortby == k { if ct.State.sortBy == k {
cm[k].colorfn = ct.colorscheme.TableHeaderColumnActiveSprintf() cm[k].colorfn = ct.colorscheme.TableHeaderColumnActiveSprintf()
if ct.sortdesc { if ct.State.sortDesc {
cm[k].arrow = "▼" cm[k].arrow = "▼"
} else { } else {
cm[k].arrow = "▲" cm[k].arrow = "▲"
@ -47,7 +47,7 @@ func (ct *Cointop) updateHeaders() {
} }
} }
if ct.portfoliovisible { if ct.State.portfolioVisible {
cols = []string{"rank", "name", "symbol", "price", cols = []string{"rank", "name", "symbol", "price",
"holdings", "balance", "24hchange", "percentholdings", "lastupdated"} "holdings", "balance", "24hchange", "percentholdings", "lastupdated"}
} else { } else {
@ -78,11 +78,11 @@ func (ct *Cointop) updateHeaders() {
} }
ct.update(func() { ct.update(func() {
if ct.headersview == nil { if ct.Views.Header.Backing == nil {
return return
} }
ct.headersview.Clear() ct.Views.Header.Backing.Clear()
fmt.Fprintln(ct.headersview, strings.Join(headers, "")) fmt.Fprintln(ct.Views.Header.Backing, strings.Join(headers, ""))
}) })
} }

@ -1,24 +0,0 @@
package cointop
// Coin is the row structure
type Coin struct {
ID string
Name string
Slug string
Symbol string
Rank int
Price float64
Volume24H float64
MarketCap float64
AvailableSupply float64
TotalSupply float64
PercentChange1H float64
PercentChange24H float64
PercentChange7D float64
LastUpdated string
// for favorites
Favorite bool
// for portfolio
Holdings float64
Balance float64
}

@ -0,0 +1,39 @@
package cointop
import (
"fmt"
"github.com/jroimartin/gocui"
)
// View is a cointop view
type View struct {
Backing *gocui.View
Name string
}
func (ct *Cointop) setActiveView(v string) error {
ct.g.SetViewOnTop(v)
ct.g.SetCurrentView(v)
if v == ct.Views.SearchField.Name {
ct.Views.SearchField.Backing.Clear()
ct.Views.SearchField.Backing.SetCursor(1, 0)
fmt.Fprintf(ct.Views.SearchField.Backing, "%s", "/")
} else if v == ct.Views.Table.Name {
ct.g.SetViewOnTop(ct.Views.Statusbar.Name)
}
if v == ct.Views.PortfolioUpdateMenu.Name {
ct.g.SetViewOnTop(ct.Views.Input.Name)
ct.g.SetCurrentView(ct.Views.Input.Name)
}
return nil
}
func (ct *Cointop) activeViewName() string {
return ct.g.CurrentView().Name()
}
func (ct *Cointop) setViewOnBottom(v string) error {
_, err := ct.g.SetViewOnBottom(v)
return err
}
Loading…
Cancel
Save