mirror of
https://github.com/miguelmota/cointop
synced 2024-11-05 00:00:14 +00:00
portfolio view
This commit is contained in:
parent
1e278fdcef
commit
f017555640
2
.gitignore
vendored
2
.gitignore
vendored
@ -37,3 +37,5 @@ build-dir
|
||||
# do not ignore .flathub
|
||||
# do not ignore .rpm
|
||||
# do not ignore .copr
|
||||
|
||||
todo.txt
|
||||
|
52
README.md
52
README.md
@ -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
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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() {
|
||||
|
@ -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
15
cointop/portfolio.go
Normal 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
36
cointop/quit.go
Normal 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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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))
|
||||
}
|
||||
|
229
cointop/table.go
229
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 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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
Loading…
Reference in New Issue
Block a user