diff --git a/.gitignore b/.gitignore
index e8af3a9..4d32b7d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -37,3 +37,5 @@ build-dir
# do not ignore .flathub
# do not ignore .rpm
# do not ignore .copr
+
+todo.txt
diff --git a/README.md b/README.md
index 6140fe8..e05976f 100644
--- a/README.md
+++ b/README.md
@@ -242,12 +242,14 @@ Key|Action
2|Sort table by *[2]4 hour change*
7|Sort table by *[7] day change*
a|Sort table by *[a]vailable supply*
+b|Sort table by *[b]alance*
c|Show currency convert menu
f|Toggle coin as favorite
F|Toggle show favorites
g|Go to first line of page (vim inspired)
G|Go to last line of page (vim inspired)
h|Go to previous page (vim inspired)
+h|Sort table by *[h]oldings* (portfolio view only)
H|Go to top of table window (vim inspired)
j|Move down (vim inspired)
k|Move up (vim inspired)
@@ -258,6 +260,7 @@ Key|Action
n|Sort table by *[n]ame*
o|[o]pen link to highlighted coin on [CoinMarketCap](https://coinmarketcap.com/)
p|Sort table by *[p]rice*
+P|Toggle show portfolio
r|Sort table by *[r]ank*
s|Sort table by *[s]ymbol*
t|Sort table by *[t]otal supply*
@@ -285,6 +288,9 @@ You can then configure the actions you want for each key:
(default `~/.cointop/config`)
```toml
+currency = "USD"
+defaultView = "default"
+
[shortcuts]
"$" = "last_page"
0 = "first_page"
@@ -297,10 +303,13 @@ You can then configure the actions you want for each key:
"]" = "next_chart_range"
"{" = "first_chart_range"
"}" = "last_chart_range"
+ C = "show_currency_convert_menu"
G = "move_to_page_last_row"
H = "move_to_page_visible_first_row"
L = "move_to_page_visible_last_row"
M = "move_to_page_visible_middle_row"
+ O = "open_link"
+ P = "toggle_portfolio"
a = "sort_column_available_supply"
"alt+down" = "sort_column_desc"
"alt+left" = "sort_left_column"
@@ -311,6 +320,7 @@ You can then configure the actions you want for each key:
right = "next_page"
up = "move_up"
c = "show_currency_convert_menu"
+ b = "sort_column_balance"
"ctrl+c" = "quit"
"ctrl+d" = "page_down"
"ctrl+f" = "open_search"
@@ -386,7 +396,9 @@ Action|Description
`sort_column_7d_change`|Sort table by column *7 day change*
`sort_column_asc`|Sort highlighted column by ascending order
`sort_column_available_supply`|Sort table by column *available supply*
+`sort_column_balance`|Sort table by column *balance*
`sort_column_desc`|Sort highlighted column by descending order
+`sort_column_holdings`|Sort table by column *holdings*
`sort_column_last_updated`|Sort table by column *last updated*
`sort_column_market_cap`|Sort table by column *market cap*
`sort_column_name`|Sort table by column *name*
@@ -400,9 +412,13 @@ Action|Description
`toggle_favorite`|Toggle coin as favorite
`toggle_show_currency_convert_menu`|Toggle show currency convert menu
`toggle_show_favorites`|Toggle show favorites
+`toggle_portfolio`|Toggle portfolio view
+`toggle_show_portfolio`|Toggle show portfolio view
## FAQ
+Frequently asked questions:
+
- Q: Where is the data from?
- A: The data is from [Coin Market Cap](https://coinmarketcap.com/).
@@ -424,9 +440,13 @@ Action|Description
export PATH=$PATH:$GOPATH/bin
```
-- Q: What is the size of the binary?
+- Q: Where is the config file located?
+
+ - A: The default configuration file is located under `~/.cointop/config`
+
+- Q: What format is the configuration file in?
- - A: The executable is only ~1.9MB in size.
+ - A: The configuration file is in [TOML](https://en.wikipedia.org/wiki/TOML) format.
- Q: How do I search?
@@ -452,6 +472,14 @@ Action|Description
- A: Press ctrl+s to save your favorites.
+- Q: What does the yellow asterisk in the row mean?
+
+ - A: The yellow asterisk or star means that you've selected that coin to be a favorite.
+
+- Q: How do I view all my portfolio?
+
+ - A: Press P (shift+p) to toggle view your portfolio.
+
- Q: I'm getting question marks or weird symbols instead of the correct characters.
- A: Make sure that your terminal has the encoding set to UTF-8 and that your terminal font supports UTF-8.
@@ -526,6 +554,10 @@ Action|Description
- A: Press ctrl+s to save the selected currency to convert to.
+- Q: What does saving do?
+
+ - A: The save command (ctrl+s) saves your selected currency, selected favorite coins, and portfolio coins to the cointop config file.
+
- Q: The data isn't refreshing!
- A: The CoinMarketCap API has rate limits, so make sure to keep manual refreshes to a minimum. If you've hit the rate limit then wait about half an hour to be able to fetch the data again. Keep in mind that CoinMarketCap updates prices every 5 minutes so constant refreshes aren't necessary.
@@ -538,6 +570,18 @@ Action|Description
- A: Press q to quit the open view/window.
+- Q: How do I set the favorites view to be the default view?
+
+ - A: In `~/.cointop/config`, set `defaultView = "favorites"`
+
+- Q: How do I set the portfolio view to be the default view?
+
+ - A: In `~/.cointop/config`, set `defaultView = "portfolio"`
+
+- Q: How do I set the table view to be the default view?
+
+ - A: In `~/.cointop/config`, set `defaultView = "default"`
+
- Q: I'm getting the error `open /dev/tty: no such device or address`.
-A: Usually this error occurs when cointop is running as a daemon or slave which means that there is no terminal allocated, so `/dev/tty` doesn't exist for that process. Try running it with the following environment variables:
@@ -546,6 +590,10 @@ Action|Description
DEV_IN=/dev/stdout DEV_OUT=/dev/stdout cointop
```
+- Q: What is the size of the binary?
+
+ - A: The executable is only ~2MB in size.
+
## Development
### Go
diff --git a/cointop/actions.go b/cointop/actions.go
index 23cbcf2..a3e585b 100644
--- a/cointop/actions.go
+++ b/cointop/actions.go
@@ -49,6 +49,8 @@ func actionsMap() map[string]bool {
"toggle_show_currency_convert_menu": true,
"show_currency_convert_menu": true,
"hide_currency_convert_menu": true,
+ "toggle_portfolio": true,
+ "toggle_show_portfolio": true,
}
}
diff --git a/cointop/cointop.go b/cointop/cointop.go
index ef97234..a2d4546 100644
--- a/cointop/cointop.go
+++ b/cointop/cointop.go
@@ -35,6 +35,8 @@ type Cointop struct {
tablecolumnorder []string
table *table.Table
maxtablewidth int
+ portfoliovisible bool
+ visible bool
statusbarview *gocui.View
statusbarviewname string
sortdesc bool
@@ -69,6 +71,18 @@ type Cointop struct {
convertmenuview *gocui.View
convertmenuviewname string
convertmenuvisible bool
+ portfolio *portfolio
+}
+
+// PortfolioEntry is portfolio entry
+type portfolioEntry struct {
+ Coin string
+ Holdings float64
+}
+
+// Portfolio is portfolio structure
+type portfolio struct {
+ Entries map[string]*portfolioEntry
}
// New initializes cointop
@@ -81,14 +95,13 @@ func New() *Cointop {
api: api.NewCMC(),
refreshticker: time.NewTicker(1 * time.Minute),
sortby: "rank",
- sortdesc: false,
page: 0,
perpage: 100,
forcerefresh: make(chan bool),
maxtablewidth: 175,
actionsmap: actionsMap(),
shortcutkeys: defaultShortcuts(),
- // DEPRECATED: favorites by 'symbol' is deprecated because of collisions.
+ // DEPRECATED: favorites by 'symbol' is deprecated because of collisions. Kept for backward compatibility.
favoritesbysymbol: map[string]bool{},
favorites: map[string]bool{},
cache: cache.New(1*time.Minute, 2*time.Minute),
@@ -142,6 +155,9 @@ func New() *Cointop {
helpviewname: "help",
convertmenuviewname: "convertmenu",
currencyconversion: "USD",
+ portfolio: &portfolio{
+ Entries: make(map[string]*portfolioEntry, 0),
+ },
}
err := ct.setupConfig()
if err != nil {
@@ -202,23 +218,3 @@ func (ct *Cointop) Run() {
log.Fatalf("main loop: %v", err)
}
}
-
-func (ct *Cointop) quit() error {
- return gocui.ErrQuit
-}
-
-func (ct *Cointop) quitView() error {
- if ct.activeViewName() == ct.tableviewname {
- return ct.quit()
- }
- return nil
-}
-
-// Exit safely exit application
-func (ct *Cointop) Exit() {
- if ct.g != nil {
- ct.g.Close()
- } else {
- os.Exit(0)
- }
-}
diff --git a/cointop/config.go b/cointop/config.go
index 6b2d5cb..a9e7cf9 100644
--- a/cointop/config.go
+++ b/cointop/config.go
@@ -13,9 +13,11 @@ import (
var fileperm = os.FileMode(0644)
type config struct {
- Shortcuts map[string]interface{} `toml:"shortcuts"`
- Favorites map[string][]interface{} `toml:"favorites"`
- Currency interface{} `toml:"currency"`
+ Shortcuts map[string]interface{} `toml:"shortcuts"`
+ Favorites map[string][]interface{} `toml:"favorites"`
+ Portfolio map[string]interface{} `toml:"portfolio"`
+ Currency interface{} `toml:"currency"`
+ DefaultView interface{} `toml:"defaultView"`
}
func (ct *Cointop) setupConfig() error {
@@ -39,31 +41,17 @@ func (ct *Cointop) setupConfig() error {
if err != nil {
return err
}
+ err = ct.loadPortfolioFromConfig()
+ if err != nil {
+ return err
+ }
err = ct.loadCurrencyFromConfig()
if err != nil {
return err
}
- return nil
-}
-
-func (ct *Cointop) loadFavoritesFromConfig() error {
- for k, arr := range ct.config.Favorites {
- // DEPRECATED: favorites by 'symbol' is deprecated because of collisions.
- if k == "symbols" {
- for _, ifc := range arr {
- v, ok := ifc.(string)
- if ok {
- ct.favoritesbysymbol[strings.ToUpper(v)] = true
- }
- }
- } else if k == "names" {
- for _, ifc := range arr {
- v, ok := ifc.(string)
- if ok {
- ct.favorites[v] = true
- }
- }
- }
+ err = ct.loadDefaultViewFromConfig()
+ if err != nil {
+ return err
}
return nil
}
@@ -153,11 +141,19 @@ func (ct *Cointop) configToToml() ([]byte, error) {
"names": favorites,
}
+ portfolioIfc := map[string]interface{}{}
+ for name := range ct.portfolio.Entries {
+ entry := ct.portfolio.Entries[name]
+ var i interface{} = entry.Holdings
+ portfolioIfc[entry.Coin] = i
+ }
+
var currencyIfc interface{} = ct.currencyconversion
var inputs = &config{
Shortcuts: shortcutsIfcs,
Favorites: favoritesIfcs,
+ Portfolio: portfolioIfc,
Currency: currencyIfc,
}
@@ -173,8 +169,7 @@ func (ct *Cointop) configToToml() ([]byte, error) {
func (ct *Cointop) loadShortcutsFromConfig() error {
for k, ifc := range ct.config.Shortcuts {
- v, ok := ifc.(string)
- if ok {
+ if v, ok := ifc.(string); ok {
if !ct.actionExists(v) {
continue
}
@@ -193,3 +188,57 @@ func (ct *Cointop) loadCurrencyFromConfig() error {
}
return nil
}
+
+func (ct *Cointop) loadDefaultViewFromConfig() error {
+ if defaultView, ok := ct.config.DefaultView.(string); ok {
+ switch defaultView {
+ case "portfolio":
+ ct.portfoliovisible = true
+ case "favorites":
+ ct.filterByFavorites = true
+ case "default":
+ fallthrough
+ default:
+ ct.portfoliovisible = false
+ ct.filterByFavorites = false
+ }
+ }
+ return nil
+}
+
+func (ct *Cointop) loadFavoritesFromConfig() error {
+ for k, arr := range ct.config.Favorites {
+ // DEPRECATED: favorites by 'symbol' is deprecated because of collisions. Kept for backward compatibility.
+ if k == "symbols" {
+ for _, ifc := range arr {
+ if v, ok := ifc.(string); ok {
+ ct.favoritesbysymbol[strings.ToUpper(v)] = true
+ }
+ }
+ } else if k == "names" {
+ for _, ifc := range arr {
+ if v, ok := ifc.(string); ok {
+ ct.favorites[v] = true
+ }
+ }
+ }
+ }
+ return nil
+}
+
+func (ct *Cointop) loadPortfolioFromConfig() error {
+ for name, holdingsIfc := range ct.config.Portfolio {
+ var holdings float64
+ var ok bool
+ if holdings, ok = holdingsIfc.(float64); !ok {
+ if holdingsInt, ok := holdingsIfc.(int64); ok {
+ holdings = float64(holdingsInt)
+ }
+ }
+ ct.portfolio.Entries[strings.ToLower(name)] = &portfolioEntry{
+ Coin: name,
+ Holdings: holdings,
+ }
+ }
+ return nil
+}
diff --git a/cointop/favorites.go b/cointop/favorites.go
index e0a5266..222547a 100644
--- a/cointop/favorites.go
+++ b/cointop/favorites.go
@@ -1,6 +1,7 @@
package cointop
func (ct *Cointop) toggleFavorite() error {
+ ct.portfoliovisible = false
coin := ct.highlightedRowCoin()
if coin == nil {
return nil
@@ -18,6 +19,7 @@ func (ct *Cointop) toggleFavorite() error {
}
func (ct *Cointop) toggleShowFavorites() error {
+ ct.portfoliovisible = false
ct.filterByFavorites = !ct.filterByFavorites
ct.updateTable()
return nil
diff --git a/cointop/headers.go b/cointop/headers.go
index 30139c1..08a1ade 100644
--- a/cointop/headers.go
+++ b/cointop/headers.go
@@ -8,58 +8,75 @@ import (
)
func (ct *Cointop) updateHeaders() {
- cm := map[string]func(a ...interface{}) string{
- "rank": color.Black,
- "name": color.Black,
- "symbol": color.Black,
- "price": color.Black,
- "marketcap": color.Black,
- "24hvolume": color.Black,
- "1hchange": color.Black,
- "24hchange": color.Black,
- "7dchange": color.Black,
- "totalsupply": color.Black,
- "availablesupply": color.Black,
- "lastupdated": color.Black,
+ var cols []string
+
+ type t struct {
+ colorfn func(a ...interface{}) string
+ displaytext string
+ padleft int
+ padright int
+ arrow string
}
- sm := map[string]string{
- "rank": " ",
- "name": " ",
- "symbol": " ",
- "price": " ",
- "marketcap": " ",
- "24hvolume": " ",
- "1hchange": " ",
- "24hchange": " ",
- "7dchange": " ",
- "totalsupply": " ",
- "availablesupply": " ",
- "lastupdated": " ",
+
+ cm := map[string]*t{
+ "rank": &t{color.Black, "[r]ank", 0, 1, " "},
+ "name": &t{color.Black, "[n]ame", 0, 11, " "},
+ "symbol": &t{color.Black, "[s]ymbol", 4, 0, " "},
+ "price": &t{color.Black, "[p]rice", 2, 0, " "},
+ "holdings": &t{color.Black, "[h]oldings", 5, 0, " "},
+ "balance": &t{color.Black, "[b]alance", 5, 0, " "},
+ "marketcap": &t{color.Black, "[m]arket cap", 5, 0, " "},
+ "24hvolume": &t{color.Black, "24H [v]olume", 3, 0, " "},
+ "1hchange": &t{color.Black, "[1]H%", 5, 0, " "},
+ "24hchange": &t{color.Black, "[2]4H%", 3, 0, " "},
+ "7dchange": &t{color.Black, "[7]D%", 4, 0, " "},
+ "totalsupply": &t{color.Black, "[t]otal supply", 7, 0, " "},
+ "availablesupply": &t{color.Black, "[a]vailable supply", 0, 0, " "},
+ "lastupdated": &t{color.Black, "last [u]pdated", 3, 0, " "},
}
+
for k := range cm {
+ cm[k].arrow = " "
if ct.sortby == k {
- cm[k] = color.CyanBg
+ cm[k].colorfn = color.CyanBg
if ct.sortdesc {
- sm[k] = "▼"
+ cm[k].arrow = "▼"
} else {
- sm[k] = "▲"
+ cm[k].arrow = "▲"
}
}
}
+
symbol := currencysymbols[ct.currencyconversion]
- headers := []string{
- fmt.Sprintf("%s%s", cm["rank"](sm["rank"]+"[r]ank"), strings.Repeat(" ", 1)),
- fmt.Sprintf("%s%s", cm["name"](sm["name"]+"[n]ame"), strings.Repeat(" ", 15)),
- fmt.Sprintf("%s%s", cm["symbol"](sm["symbol"]+"[s]ymbol"), strings.Repeat(" ", 1)),
- fmt.Sprintf("%s%s", strings.Repeat(" ", 0), cm["price"](sm["price"]+symbol+"[p]rice")),
- fmt.Sprintf("%s%s", strings.Repeat(" ", 5), cm["marketcap"](sm["marketcap"]+"[m]arket cap")),
- fmt.Sprintf("%s%s", strings.Repeat(" ", 3), cm["24hvolume"](sm["24hvolume"]+"24H [v]olume")),
- fmt.Sprintf("%s%s", strings.Repeat(" ", 4), cm["1hchange"](sm["1hchange"]+"[1]H%")),
- fmt.Sprintf("%s%s", strings.Repeat(" ", 3), cm["24hchange"](sm["24hchange"]+"[2]4H%")),
- fmt.Sprintf("%s%s", strings.Repeat(" ", 3), cm["7dchange"](sm["7dchange"]+"[7]DH%")),
- fmt.Sprintf("%s%s", strings.Repeat(" ", 6), cm["totalsupply"](sm["totalsupply"]+"[t]otal supply")),
- fmt.Sprintf("%s%s", strings.Repeat(" ", 1), cm["availablesupply"](sm["availablesupply"]+"[a]vailable supply")),
- fmt.Sprintf("%s%s", strings.Repeat(" ", 4), cm["lastupdated"](sm["lastupdated"]+"last [u]pdated")),
+
+ if ct.portfoliovisible {
+ cols = []string{"rank", "name", "symbol", "price",
+ "holdings", "balance", "24hchange", "lastupdated"}
+ } else {
+ cols = []string{"rank", "name", "symbol", "price",
+ "marketcap", "24hvolume", "1hchange", "24hchange",
+ "7dchange", "totalsupply", "availablesupply", "lastupdated"}
+ }
+
+ var headers []string
+ for _, v := range cols {
+ s, ok := cm[v]
+ if !ok {
+ continue
+ }
+ var str string
+ d := s.arrow + s.displaytext
+ if v == "price" || v == "balance" {
+ d = s.arrow + symbol + s.displaytext
+ }
+
+ str = fmt.Sprintf(
+ "%s%s%s",
+ strings.Repeat(" ", s.padleft),
+ s.colorfn(d),
+ strings.Repeat(" ", s.padright),
+ )
+ headers = append(headers, str)
}
ct.update(func() {
diff --git a/cointop/keybindings.go b/cointop/keybindings.go
index 5df8082..ce4797e 100644
--- a/cointop/keybindings.go
+++ b/cointop/keybindings.go
@@ -201,6 +201,9 @@ func (ct *Cointop) parseKeys(s string) (interface{}, gocui.Modifier) {
func (ct *Cointop) keybindings(g *gocui.Gui) error {
for k, v := range ct.shortcutkeys {
+ if k == "" {
+ continue
+ }
v = strings.TrimSpace(strings.ToLower(v))
var fn func(g *gocui.Gui, v *gocui.View) error
key, mod := ct.parseKeys(k)
@@ -211,7 +214,7 @@ func (ct *Cointop) keybindings(g *gocui.Gui) error {
case "move_down":
fn = ct.keyfn(ct.cursorDown)
case "previous_page":
- fn = ct.keyfn(ct.prevPage)
+ fn = ct.handleHkey()
case "next_page":
fn = ct.keyfn(ct.nextPage)
case "page_down":
@@ -268,7 +271,7 @@ func (ct *Cointop) keybindings(g *gocui.Gui) error {
case "move_to_page_visible_middle_row":
fn = ct.keyfn(ct.navigatePageMiddleLine)
case "sort_column_name":
- fn = ct.sortfn("name", true)
+ fn = ct.sortfn("name", false)
case "sort_column_price":
fn = ct.sortfn("price", true)
case "sort_column_rank":
@@ -279,6 +282,10 @@ func (ct *Cointop) keybindings(g *gocui.Gui) error {
fn = ct.sortfn("lastupdated", true)
case "sort_column_24h_volume":
fn = ct.sortfn("24hvolume", true)
+ case "sort_column_balance":
+ fn = ct.sortfn("balance", true)
+ case "sort_column_holdings":
+ fn = ct.sortfn("holdings", true)
case "last_page":
fn = ct.keyfn(ct.lastPage)
case "open_search":
@@ -310,6 +317,10 @@ func (ct *Cointop) keybindings(g *gocui.Gui) error {
case "hide_currency_convert_menu":
fn = ct.keyfn(ct.hideConvertMenu)
view = "convertmenu"
+ case "toggle_portfolio":
+ fn = ct.keyfn(ct.togglePortfolio)
+ case "toggle_show_portfolio":
+ fn = ct.keyfn(ct.toggleShowPortfolio)
default:
fn = ct.keyfn(ct.noop)
}
@@ -360,6 +371,17 @@ func (ct *Cointop) keyfn(fn func() error) func(g *gocui.Gui, v *gocui.View) erro
}
}
+func (ct *Cointop) handleHkey() func(g *gocui.Gui, v *gocui.View) error {
+ return func(g *gocui.Gui, v *gocui.View) error {
+ if ct.portfoliovisible {
+ ct.sortToggle("holdings", true)
+ } else {
+ ct.prevPage()
+ }
+ return nil
+ }
+}
+
func (ct *Cointop) noop() error {
return nil
}
diff --git a/cointop/portfolio.go b/cointop/portfolio.go
new file mode 100644
index 0000000..9315bd1
--- /dev/null
+++ b/cointop/portfolio.go
@@ -0,0 +1,15 @@
+package cointop
+
+func (ct *Cointop) togglePortfolio() error {
+ ct.filterByFavorites = false
+ ct.portfoliovisible = !ct.portfoliovisible
+ ct.updateTable()
+ return nil
+}
+
+func (ct *Cointop) toggleShowPortfolio() error {
+ ct.filterByFavorites = false
+ ct.portfoliovisible = true
+ ct.updateTable()
+ return nil
+}
diff --git a/cointop/quit.go b/cointop/quit.go
new file mode 100644
index 0000000..60e8efa
--- /dev/null
+++ b/cointop/quit.go
@@ -0,0 +1,36 @@
+package cointop
+
+import (
+ "os"
+
+ "github.com/miguelmota/cointop/pkg/gocui"
+)
+
+func (ct *Cointop) quit() error {
+ return gocui.ErrQuit
+}
+
+func (ct *Cointop) quitView() error {
+ if ct.portfoliovisible {
+ ct.portfoliovisible = false
+ return ct.updateTable()
+ }
+ if ct.filterByFavorites {
+ ct.filterByFavorites = false
+ return ct.updateTable()
+ }
+ if ct.activeViewName() == ct.tableviewname {
+ return ct.quit()
+ }
+
+ return nil
+}
+
+// Exit safely exit application
+func (ct *Cointop) Exit() {
+ if ct.g != nil {
+ ct.g.Close()
+ } else {
+ os.Exit(0)
+ }
+}
diff --git a/cointop/save.go b/cointop/save.go
index 158441e..460331a 100644
--- a/cointop/save.go
+++ b/cointop/save.go
@@ -1,8 +1,12 @@
package cointop
+import "log"
+
func (ct *Cointop) save() error {
ct.setSavingStatus()
- ct.saveConfig()
+ if err := ct.saveConfig(); err != nil {
+ log.Fatal(err)
+ }
return nil
}
diff --git a/cointop/shortcuts.go b/cointop/shortcuts.go
index 0b38b71..7e6645e 100644
--- a/cointop/shortcuts.go
+++ b/cointop/shortcuts.go
@@ -35,6 +35,7 @@ func defaultShortcuts() map[string]string {
"2": "sort_column_24h_change",
"7": "sort_column_7d_change",
"a": "sort_column_available_supply",
+ "b": "sort_column_balance",
"c": "show_currency_convert_menu",
"C": "show_currency_convert_menu",
"f": "toggle_favorite",
@@ -51,7 +52,9 @@ func defaultShortcuts() map[string]string {
"M": "move_to_page_visible_middle_row",
"n": "sort_column_name",
"o": "open_link",
+ "O": "open_link",
"p": "sort_column_price",
+ "P": "toggle_portfolio",
"r": "sort_column_rank",
"s": "sort_column_symbol",
"t": "sort_column_total_supply",
diff --git a/cointop/sort.go b/cointop/sort.go
index fd712e0..5a2b4d9 100644
--- a/cointop/sort.go
+++ b/cointop/sort.go
@@ -24,6 +24,10 @@ func (ct *Cointop) sort(sortby string, desc bool, list []*coin) {
return a.Symbol < b.Symbol
case "price":
return a.Price < b.Price
+ case "holdings":
+ return a.Holdings < b.Holdings
+ case "balance":
+ return a.Balance < b.Balance
case "marketcap":
return a.MarketCap < b.MarketCap
case "24hvolume":
@@ -47,15 +51,19 @@ func (ct *Cointop) sort(sortby string, desc bool, list []*coin) {
ct.updateHeaders()
}
+func (ct *Cointop) sortToggle(sortby string, desc bool) error {
+ if ct.sortby == sortby {
+ desc = !ct.sortdesc
+ }
+
+ ct.sort(sortby, desc, ct.coins)
+ 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 {
- if ct.sortby == sortby {
- desc = !desc
- }
-
- ct.sort(sortby, desc, ct.coins)
- ct.updateTable()
- return nil
+ return ct.sortToggle(sortby, desc)
}
}
diff --git a/cointop/statusbar.go b/cointop/statusbar.go
index 5291b95..48d7417 100644
--- a/cointop/statusbar.go
+++ b/cointop/statusbar.go
@@ -11,7 +11,7 @@ func (ct *Cointop) updateStatusbar(s string) {
ct.statusbarview.Clear()
currpage := ct.currentDisplayPage()
totalpages := ct.totalPages()
- base := fmt.Sprintf("%sQuit %sHelp %sChart %sRange %sSearch %sConvert %sFavorite %sSave", "[Q]", "[?]", "[Enter]", "[[ ]]", "[/]", "[C]", "[F]", "[CTRL-S]")
+ base := fmt.Sprintf("%sQuit %sHelp %sChart %sRange %sSearch %sConvert %sFavorites %sPortfolio %sSave", "[Q]", "[?]", "[Enter]", "[[ ]]", "[/]", "[C]", "[F]", "[P]", "[CTRL-S]")
str := pad.Right(fmt.Sprintf("%v %sPage %v/%v %s", base, "[← →]", currpage, totalpages, s), ct.maxtablewidth, " ")
v := fmt.Sprintf("v%s", ct.version())
str = str[:len(str)-len(v)+2] + v
@@ -20,6 +20,6 @@ func (ct *Cointop) updateStatusbar(s string) {
}
func (ct *Cointop) refreshRowLink() {
- url := ct.rowLink()
+ url := ct.rowLinkShort()
ct.updateStatusbar(fmt.Sprintf("%sOpen %s", "[O]", url))
}
diff --git a/cointop/table.go b/cointop/table.go
index 9be2178..bc8100f 100644
--- a/cointop/table.go
+++ b/cointop/table.go
@@ -3,6 +3,7 @@ package cointop
import (
"fmt"
"math"
+ "sort"
"strconv"
"strings"
"time"
@@ -16,73 +17,117 @@ import (
func (ct *Cointop) refreshTable() error {
maxX := ct.width()
ct.table = table.New().SetWidth(maxX)
- ct.table.AddCol("")
- ct.table.AddCol("")
- ct.table.AddCol("")
- ct.table.AddCol("")
- ct.table.AddCol("")
- ct.table.AddCol("")
- ct.table.AddCol("")
- ct.table.AddCol("")
- ct.table.AddCol("")
- ct.table.AddCol("")
- ct.table.AddCol("")
- ct.table.AddCol("")
ct.table.HideColumHeaders = true
- for _, coin := range ct.coins {
- unix, _ := strconv.ParseInt(coin.LastUpdated, 10, 64)
- lastUpdated := time.Unix(unix, 0).Format("15:04:05 Jan 02")
- namecolor := color.White
- colorprice := color.Cyan
- color1h := color.White
- color24h := color.White
- color7d := color.White
- if coin.Favorite {
- namecolor = color.Yellow
- }
- if coin.PercentChange1H > 0 {
- color1h = color.Green
- }
- if coin.PercentChange1H < 0 {
- color1h = color.Red
- }
- if coin.PercentChange24H > 0 {
- color24h = color.Green
- }
- if coin.PercentChange24H < 0 {
- color24h = color.Red
- }
- if coin.PercentChange7D > 0 {
- color7d = color.Green
- }
- if coin.PercentChange7D < 0 {
- color7d = color.Red
- }
- name := coin.Name
- dots := "..."
- star := " "
- if coin.Favorite {
- star = "*"
+
+ if ct.portfoliovisible {
+ ct.table.AddCol("")
+ ct.table.AddCol("")
+ ct.table.AddCol("")
+ ct.table.AddCol("")
+ ct.table.AddCol("")
+ ct.table.AddCol("")
+ ct.table.AddCol("")
+ ct.table.AddCol("")
+ for _, coin := range ct.coins {
+ unix, _ := strconv.ParseInt(coin.LastUpdated, 10, 64)
+ lastUpdated := time.Unix(unix, 0).Format("15:04:05 Jan 02")
+ namecolor := color.White
+ colorprice := color.White
+ colorbalance := color.Cyan
+ color24h := color.White
+ if coin.PercentChange24H > 0 {
+ color24h = color.Green
+ }
+ if coin.PercentChange24H < 0 {
+ color24h = color.Red
+ }
+ name := coin.Name
+ dots := "..."
+ star := " "
+ rank := fmt.Sprintf("%s%v", color.Yellow(star), color.White(fmt.Sprintf("%6v ", coin.Rank)))
+ if len(name) > 20 {
+ name = fmt.Sprintf("%s%s", name[0:18], dots)
+ }
+
+ ct.table.AddRow(
+ rank,
+ namecolor(pad.Right(fmt.Sprintf("%.22s", name), 21, " ")),
+ color.White(pad.Right(fmt.Sprintf("%.6s", coin.Symbol), 5, " ")),
+ colorprice(fmt.Sprintf("%13s", humanize.Commaf(coin.Price))),
+ color.White(fmt.Sprintf("%15s", humanize.Commaf(coin.Holdings))),
+ colorbalance(fmt.Sprintf("%15s", humanize.Commaf(coin.Balance))),
+ color24h(fmt.Sprintf("%8.2f%%", coin.PercentChange24H)),
+ color.White(pad.Right(fmt.Sprintf("%17s", lastUpdated), 80, " ")),
+ )
}
- rank := fmt.Sprintf("%s%v", color.Yellow(star), color.White(fmt.Sprintf("%6v ", coin.Rank)))
- if len(name) > 20 {
- name = fmt.Sprintf("%s%s", name[0:18], dots)
+ } else {
+ ct.table.AddCol("")
+ ct.table.AddCol("")
+ ct.table.AddCol("")
+ ct.table.AddCol("")
+ ct.table.AddCol("")
+ ct.table.AddCol("")
+ ct.table.AddCol("")
+ ct.table.AddCol("")
+ ct.table.AddCol("")
+ ct.table.AddCol("")
+ ct.table.AddCol("")
+ ct.table.AddCol("")
+ for _, coin := range ct.coins {
+ unix, _ := strconv.ParseInt(coin.LastUpdated, 10, 64)
+ lastUpdated := time.Unix(unix, 0).Format("15:04:05 Jan 02")
+ namecolor := color.White
+ colorprice := color.Cyan
+ color1h := color.White
+ color24h := color.White
+ color7d := color.White
+ if coin.Favorite {
+ namecolor = color.Yellow
+ }
+ if coin.PercentChange1H > 0 {
+ color1h = color.Green
+ }
+ if coin.PercentChange1H < 0 {
+ color1h = color.Red
+ }
+ if coin.PercentChange24H > 0 {
+ color24h = color.Green
+ }
+ if coin.PercentChange24H < 0 {
+ color24h = color.Red
+ }
+ if coin.PercentChange7D > 0 {
+ color7d = color.Green
+ }
+ if coin.PercentChange7D < 0 {
+ color7d = color.Red
+ }
+ name := coin.Name
+ dots := "..."
+ star := " "
+ if coin.Favorite {
+ star = "*"
+ }
+ rank := fmt.Sprintf("%s%v", color.Yellow(star), color.White(fmt.Sprintf("%6v ", coin.Rank)))
+ if len(name) > 20 {
+ name = fmt.Sprintf("%s%s", name[0:18], dots)
+ }
+ ct.table.AddRow(
+ rank,
+ namecolor(pad.Right(fmt.Sprintf("%.22s", name), 21, " ")),
+ color.White(pad.Right(fmt.Sprintf("%.6s", coin.Symbol), 5, " ")),
+ colorprice(fmt.Sprintf("%12s", humanize.Commaf(coin.Price))),
+ color.White(fmt.Sprintf("%17s", humanize.Commaf(coin.MarketCap))),
+ color.White(fmt.Sprintf("%15s", humanize.Commaf(coin.Volume24H))),
+ color1h(fmt.Sprintf("%8.2f%%", coin.PercentChange1H)),
+ color24h(fmt.Sprintf("%8.2f%%", coin.PercentChange24H)),
+ color7d(fmt.Sprintf("%8.2f%%", coin.PercentChange7D)),
+ color.White(fmt.Sprintf("%21s", humanize.Commaf(coin.TotalSupply))),
+ color.White(fmt.Sprintf("%18s", humanize.Commaf(coin.AvailableSupply))),
+ color.White(fmt.Sprintf("%18s", lastUpdated)),
+ // TODO: add %percent of cap
+ )
}
- ct.table.AddRow(
- rank,
- namecolor(pad.Right(fmt.Sprintf("%.22s", name), 21, " ")),
- color.White(pad.Right(fmt.Sprintf("%.6s", coin.Symbol), 5, " ")),
- colorprice(fmt.Sprintf("%12s", humanize.Commaf(coin.Price))),
- color.White(fmt.Sprintf("%17s", humanize.Commaf(coin.MarketCap))),
- color.White(fmt.Sprintf("%15s", humanize.Commaf(coin.Volume24H))),
- color1h(fmt.Sprintf("%8.2f%%", coin.PercentChange1H)),
- color24h(fmt.Sprintf("%8.2f%%", coin.PercentChange24H)),
- color7d(fmt.Sprintf("%8.2f%%", coin.PercentChange7D)),
- color.White(fmt.Sprintf("%21s", humanize.Commaf(coin.TotalSupply))),
- color.White(fmt.Sprintf("%18s", humanize.Commaf(coin.AvailableSupply))),
- color.White(fmt.Sprintf("%18s", lastUpdated)),
- // add %percent of cap
- )
}
// highlight last row if current row is out of bounds (can happen when switching views)
@@ -123,6 +168,51 @@ func (ct *Cointop) updateTable() error {
return nil
}
+ if ct.portfoliovisible {
+ for i := range ct.allcoins {
+ if len(ct.portfolio.Entries) == 0 {
+ break
+ }
+ coin := ct.allcoins[i]
+ var p *portfolioEntry
+ var ok bool
+ if p, ok = ct.portfolio.Entries[strings.ToLower(coin.Name)]; !ok {
+ // NOTE: if not found then try the symbol
+ if p, ok = ct.portfolio.Entries[strings.ToLower(coin.Symbol)]; !ok {
+ continue
+ }
+ }
+ holdingsstr := fmt.Sprintf("%.2f", p.Holdings)
+ if ct.currencyconversion == "ETH" || ct.currencyconversion == "BTC" {
+ holdingsstr = fmt.Sprintf("%.5f", p.Holdings)
+ }
+ holdings, _ := strconv.ParseFloat(holdingsstr, 64)
+ coin.Holdings = holdings
+
+ balance := coin.Price * p.Holdings
+ balancestr := fmt.Sprintf("%.2f", balance)
+ if ct.currencyconversion == "ETH" || ct.currencyconversion == "BTC" {
+ balancestr = fmt.Sprintf("%.5f", balance)
+ }
+ balance, _ = strconv.ParseFloat(balancestr, 64)
+ coin.Balance = balance
+ sliced = append(sliced, coin)
+ }
+
+ sort.Slice(sliced, func(i, j int) bool {
+ return sliced[i].Balance > sliced[j].Balance
+ })
+
+ for i, coin := range sliced {
+ coin.Rank = i + 1
+ }
+
+ ct.coins = sliced
+ ct.sort(ct.sortby, ct.sortdesc, ct.coins)
+ ct.refreshTable()
+ return nil
+ }
+
start := ct.page * ct.perpage
end := start + ct.perpage
allcoins := ct.allCoins()
@@ -182,9 +272,20 @@ func (ct *Cointop) rowLink() string {
return ""
}
slug := strings.ToLower(strings.Replace(coin.Name, " ", "-", -1))
+ // TODO: dynamic
return fmt.Sprintf("https://coinmarketcap.com/currencies/%s", slug)
}
+func (ct *Cointop) rowLinkShort() string {
+ coin := ct.highlightedRowCoin()
+ if coin == nil {
+ return ""
+ }
+ // TODO: dynamic
+ slug := strings.ToLower(strings.Replace(coin.Name, " ", "-", -1))
+ return fmt.Sprintf("http://coinmarketcap.com/.../%s", slug)
+}
+
func (ct *Cointop) allCoins() []*coin {
if ct.filterByFavorites {
var list []*coin
diff --git a/cointop/types.go b/cointop/types.go
index 4e624ed..01d4d6a 100644
--- a/cointop/types.go
+++ b/cointop/types.go
@@ -15,5 +15,9 @@ type coin struct {
PercentChange24H float64
PercentChange7D float64
LastUpdated string
- Favorite bool
+ // for favorites
+ Favorite bool
+ // for portfolio
+ Holdings float64
+ Balance float64
}
diff --git a/pkg/api/impl/coinmarketcap/coinmarketcap.go b/pkg/api/impl/coinmarketcap/coinmarketcap.go
index 5651119..e29e6c8 100644
--- a/pkg/api/impl/coinmarketcap/coinmarketcap.go
+++ b/pkg/api/impl/coinmarketcap/coinmarketcap.go
@@ -75,7 +75,7 @@ func (s *Service) GetAllCoinData(convert string) (map[string]apitypes.Coin, erro
for _, v := range coins {
price := v.Quotes[convert].Price
pricestr := fmt.Sprintf("%.2f", price)
- if convert == "ETH" || convert == "BTC" {
+ if convert == "ETH" || convert == "BTC" || price < 1 {
pricestr = fmt.Sprintf("%.5f", price)
}
price, _ = strconv.ParseFloat(pricestr, 64)
diff --git a/pkg/color/color.go b/pkg/color/color.go
index 40ddbfe..060c46c 100644
--- a/pkg/color/color.go
+++ b/pkg/color/color.go
@@ -2,6 +2,9 @@ package color
import "github.com/fatih/color"
+// Color struct
+type Color color.Color
+
var (
// Black color
Black = color.New(color.FgBlack).SprintFunc()