2
0
mirror of https://github.com/miguelmota/cointop synced 2024-11-05 00:00:14 +00:00

portfolio view

This commit is contained in:
Miguel Mota 2018-12-23 00:10:41 -08:00
parent 1e278fdcef
commit f017555640
18 changed files with 482 additions and 170 deletions

2
.gitignore vendored
View File

@ -37,3 +37,5 @@ build-dir
# do not ignore .flathub
# do not ignore .rpm
# do not ignore .copr
todo.txt

View File

@ -242,12 +242,14 @@ Key|Action
<kbd>2</kbd>|Sort table by *[2]4 hour change*
<kbd>7</kbd>|Sort table by *[7] day change*
<kbd>a</kbd>|Sort table by *[a]vailable supply*
<kbd>b</kbd>|Sort table by *[b]alance*
<kbd>c</kbd>|Show currency convert menu
<kbd>f</kbd>|Toggle coin as favorite
<kbd>F</kbd>|Toggle show favorites
<kbd>g</kbd>|Go to first line of page (vim inspired)
<kbd>G</kbd>|Go to last line of page (vim inspired)
<kbd>h</kbd>|Go to previous page (vim inspired)
<kbd>h</kbd>|Sort table by *[h]oldings* (portfolio view only)
<kbd>H</kbd>|Go to top of table window (vim inspired)
<kbd>j</kbd>|Move down (vim inspired)
<kbd>k</kbd>|Move up (vim inspired)
@ -258,6 +260,7 @@ Key|Action
<kbd>n</kbd>|Sort table by *[n]ame*
<kbd>o</kbd>|[o]pen link to highlighted coin on [CoinMarketCap](https://coinmarketcap.com/)
<kbd>p</kbd>|Sort table by *[p]rice*
<kbd>P</kbd>|Toggle show portfolio
<kbd>r</kbd>|Sort table by *[r]ank*
<kbd>s</kbd>|Sort table by *[s]ymbol*
<kbd>t</kbd>|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 executable is only ~1.9MB in size.
- A: The default configuration file is located under `~/.cointop/config`
- Q: What format is the configuration file in?
- 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 <kbd>ctrl</kbd>+<kbd>s</kbd> 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 <kbd>P</kbd> (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 <kbd>ctrl</kbd>+<kbd>s</kbd> to save the selected currency to convert to.
- Q: What does saving do?
- A: The save command (<kbd>ctrl</kbd>+<kbd>s</kbd>) 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 <kbd>q</kbd> 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

View File

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

View File

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

View File

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

View File

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

View File

@ -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() {

View File

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

15
cointop/portfolio.go Normal file
View File

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

36
cointop/quit.go Normal file
View File

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

View File

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

View File

@ -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",

View File

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

View File

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

View File

@ -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 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, " ")),
)
}
if coin.PercentChange1H > 0 {
color1h = color.Green
} 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
)
}
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)),
// 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

View File

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

View File

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

View File

@ -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()