portfolio add/edit menu

pull/22/head
Miguel Mota 5 years ago
parent f017555640
commit caf7a757d3

@ -29,7 +29,7 @@ In action
- [Features](#features)
- [Installing](#install)
- [Updating](#updating)
- [Usage](#usage)
- [Getting started](#getting-started)
- [Shortcuts](#shortcuts)
- [Config](#config)
- [FAQ](#faq)
@ -47,11 +47,12 @@ In action
- Fuzzy searching for finding coins
- Currency conversion
- Save and view favorite coins
- Portfolio tracking of holdings
- Color support
- Help menu
- Offline cache
- Works on macOS, Linux, and Windows
- It's very lightweight; can be left running for weeks
- It's very lightweight; can be left running indefinitely
## Installing
@ -198,7 +199,7 @@ Use the `refresh` command to update snap.
sudo snap refresh cointop --stable
```
## Usage
## Getting started
Just run the `cointop` command to get started:
@ -206,6 +207,26 @@ Just run the `cointop` command to get started:
$ cointop
```
### Navigation
- Easiest way to navigate up and down is using the arrow keys <kbd></kbd> and <kbd></kbd>
- To go the next and previous pages, use <kbd></kbd> and <kbd></kbd>
- To go to the top and bottom of the page, use <kbd>g</kbd> and <kbd>G</kbd>
- Check out the rest of [shortcut](#shortcuts) keys for vim-inspired navigation
### Favorites
- To toggle a coin as a favorite, press <kbd>Space</kbd> on the highlighted coin
- To view all your favorite coins, press <kbd>F<kbd>
- To exit out of the favorites view, press <kbd>F</kbd> again or <kbd>q</kbd>
### Portfolio
- To add a coin to your portfolio, press <kbd>e</kbd> on the highlighted coin
- To edit the holdings of coin in your portfolio, press <kbd>e</kbd> on the highlighted coin
- To view your portfolio, press <kbd>P<kbd>
- To exit out of the portfolio view press, <kbd>P</kbd> again or <kbd>q</kbd>
## Shortcuts
List of default shortcut keys:
@ -244,6 +265,9 @@ Key|Action
<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>C</kbd>|Show currency convert menu
<kbd>e</kbd>|Show portfolio edit holdings menu
<kbd>E</kbd>|Show portfolio edit holdings 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)
@ -304,6 +328,7 @@ defaultView = "default"
"{" = "first_chart_range"
"}" = "last_chart_range"
C = "show_currency_convert_menu"
E = "show_portfolio_edit_menu"
G = "move_to_page_last_row"
H = "move_to_page_visible_first_row"
L = "move_to_page_visible_last_row"
@ -329,6 +354,7 @@ defaultView = "default"
"ctrl+r" = "refresh"
"ctrl+s" = "save"
"ctrl+u" = "page_up"
e = "show_portfolio_edit_menu"
end = "move_to_page_last_row"
enter = "toggle_row_chart"
esc = "quit"
@ -414,6 +440,7 @@ Action|Description
`toggle_show_favorites`|Toggle show favorites
`toggle_portfolio`|Toggle portfolio view
`toggle_show_portfolio`|Toggle show portfolio view
`show_portfolio_edit_menu`|Show portfolio edit holdings menu
## FAQ
@ -476,7 +503,19 @@ Frequently asked questions:
- 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?
- Q: How do I add a coin to my portfolio?
- Press <kbd>e</kbd> on the highlighted coin to enter holdings and add to your portfolio.
- Q: How do I edit the holdings of a coin in my portfolio?
- Press <kbd>e</kbd> on the highlighted coin to edit the holdings.
- Q: How do I remove a coin in my portfolio?
- Press <kbd>e</kbd> on the highlighted coin to edit the holdings and set the value to any empty string (blank value). Set it to `0` if you want to keep the coin without a value.
- Q: How do I view my portfolio?
- A: Press <kbd>P</kbd> (shift+p) to toggle view your portfolio.

@ -58,20 +58,26 @@ type Cointop struct {
searchfieldviewname string
searchfieldvisible bool
// DEPRECATED: favorites by 'symbol' is deprecated because of collisions.
favoritesbysymbol map[string]bool
favorites map[string]bool
filterByFavorites bool
savemux sync.Mutex
cache *cache.Cache
debug bool
helpview *gocui.View
helpviewname string
helpvisible bool
currencyconversion string
convertmenuview *gocui.View
convertmenuviewname string
convertmenuvisible bool
portfolio *portfolio
favoritesbysymbol map[string]bool
favorites map[string]bool
filterByFavorites bool
savemux sync.Mutex
cache *cache.Cache
debug bool
helpview *gocui.View
helpviewname string
helpvisible bool
currencyconversion string
convertmenuview *gocui.View
convertmenuviewname string
convertmenuvisible bool
portfolio *portfolio
portfolioupdatemenuview *gocui.View
portfolioupdatemenuviewname string
portfolioupdatemenuvisible bool
inputview *gocui.View
inputviewname string
defaultView string
}
// PortfolioEntry is portfolio entry
@ -142,6 +148,8 @@ func New() *Cointop {
"name",
"symbol",
"price",
"holdings",
"balance",
"marketcap",
"24hvolume",
"1hchange",
@ -158,6 +166,8 @@ func New() *Cointop {
portfolio: &portfolio{
Entries: make(map[string]*portfolioEntry, 0),
},
portfolioupdatemenuviewname: "portfolioupdatemenu",
inputviewname: "input",
}
err := ct.setupConfig()
if err != nil {

@ -143,18 +143,23 @@ func (ct *Cointop) configToToml() ([]byte, error) {
portfolioIfc := map[string]interface{}{}
for name := range ct.portfolio.Entries {
entry := ct.portfolio.Entries[name]
entry, ok := ct.portfolio.Entries[name]
if !ok || entry.Coin == "" {
continue
}
var i interface{} = entry.Holdings
portfolioIfc[entry.Coin] = i
}
var currencyIfc interface{} = ct.currencyconversion
var defaultViewIfc interface{} = ct.defaultView
var inputs = &config{
Shortcuts: shortcutsIfcs,
Favorites: favoritesIfcs,
Portfolio: portfolioIfc,
Currency: currencyIfc,
Shortcuts: shortcutsIfcs,
Favorites: favoritesIfcs,
Portfolio: portfolioIfc,
Currency: currencyIfc,
DefaultView: defaultViewIfc,
}
var b bytes.Buffer
@ -191,6 +196,7 @@ func (ct *Cointop) loadCurrencyFromConfig() error {
func (ct *Cointop) loadDefaultViewFromConfig() error {
if defaultView, ok := ct.config.DefaultView.(string); ok {
defaultView = strings.ToLower(defaultView)
switch defaultView {
case "portfolio":
ct.portfoliovisible = true
@ -201,7 +207,9 @@ func (ct *Cointop) loadDefaultViewFromConfig() error {
default:
ct.portfoliovisible = false
ct.filterByFavorites = false
defaultView = "default"
}
ct.defaultView = defaultView
}
return nil
}
@ -235,10 +243,8 @@ func (ct *Cointop) loadPortfolioFromConfig() error {
holdings = float64(holdingsInt)
}
}
ct.portfolio.Entries[strings.ToLower(name)] = &portfolioEntry{
Coin: name,
Holdings: holdings,
}
ct.setPortfolioEntry(name, holdings)
}
return nil
}

@ -214,7 +214,7 @@ func (ct *Cointop) keybindings(g *gocui.Gui) error {
case "move_down":
fn = ct.keyfn(ct.cursorDown)
case "previous_page":
fn = ct.handleHkey()
fn = ct.handleHkey(key)
case "next_page":
fn = ct.keyfn(ct.nextPage)
case "page_down":
@ -321,6 +321,8 @@ func (ct *Cointop) keybindings(g *gocui.Gui) error {
fn = ct.keyfn(ct.togglePortfolio)
case "toggle_show_portfolio":
fn = ct.keyfn(ct.toggleShowPortfolio)
case "show_portfolio_edit_menu":
fn = ct.keyfn(ct.togglePortfolioUpdateMenu)
default:
fn = ct.keyfn(ct.noop)
}
@ -340,6 +342,13 @@ func (ct *Cointop) keybindings(g *gocui.Gui) error {
ct.setKeybindingMod(gocui.KeyEsc, gocui.ModNone, ct.keyfn(ct.hideHelp), ct.helpviewname)
ct.setKeybindingMod('q', gocui.ModNone, ct.keyfn(ct.hideHelp), ct.helpviewname)
// keys to quit portfolio update menu when open
ct.setKeybindingMod(gocui.KeyEsc, gocui.ModNone, ct.keyfn(ct.hidePortfolioUpdateMenu), ct.inputviewname)
ct.setKeybindingMod('q', gocui.ModNone, ct.keyfn(ct.hidePortfolioUpdateMenu), ct.inputviewname)
// keys to update portfolio holdings
ct.setKeybindingMod(gocui.KeyEnter, gocui.ModNone, ct.keyfn(ct.setPortfolioHoldings), ct.inputviewname)
// keys to quit convert menu when open
ct.setKeybindingMod(gocui.KeyEsc, gocui.ModNone, ct.keyfn(ct.hideConvertMenu), ct.convertmenuviewname)
ct.setKeybindingMod('q', gocui.ModNone, ct.keyfn(ct.hideConvertMenu), ct.convertmenuviewname)
@ -371,9 +380,9 @@ 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 {
func (ct *Cointop) handleHkey(key interface{}) func(g *gocui.Gui, v *gocui.View) error {
return func(g *gocui.Gui, v *gocui.View) error {
if ct.portfoliovisible {
if k, ok := key.(rune); ok && k == 'h' && ct.portfoliovisible {
ct.sortToggle("holdings", true)
} else {
ct.prevPage()

@ -115,6 +115,28 @@ func (ct *Cointop) layout(g *gocui.Gui) error {
ct.helpview.FgColor = gocui.ColorWhite
}
if v, err := g.SetView(ct.portfolioupdatemenuviewname, 1, 1, ct.maxtablewidth-2, maxY-1); err != nil {
if err != gocui.ErrUnknownView {
return err
}
ct.portfolioupdatemenuview = v
ct.portfolioupdatemenuview.Frame = false
ct.portfolioupdatemenuview.BgColor = gocui.ColorBlack
ct.portfolioupdatemenuview.FgColor = gocui.ColorWhite
}
if v, err := g.SetView(ct.inputviewname, 3, 6, 30, 8); err != nil {
if err != gocui.ErrUnknownView {
return err
}
ct.inputview = v
ct.inputview.Frame = true
ct.inputview.Editable = true
ct.inputview.Wrap = true
ct.inputview.BgColor = gocui.ColorBlack
ct.inputview.FgColor = gocui.ColorWhite
}
if v, err := g.SetView(ct.convertmenuviewname, 1, 1, ct.maxtablewidth-2, maxY-1); err != nil {
if err != gocui.ErrUnknownView {
return err
@ -127,9 +149,11 @@ func (ct *Cointop) layout(g *gocui.Gui) error {
// run only once on init.
// this bit of code should be at the bottom
ct.g = g
g.SetViewOnBottom(ct.searchfieldviewname) // hide
g.SetViewOnBottom(ct.helpviewname) // hide
g.SetViewOnBottom(ct.convertmenuviewname) // hide
g.SetViewOnBottom(ct.searchfieldviewname) // hide
g.SetViewOnBottom(ct.helpviewname) // hide
g.SetViewOnBottom(ct.convertmenuviewname) // hide
g.SetViewOnBottom(ct.portfolioupdatemenuviewname) // hide
g.SetViewOnBottom(ct.inputviewname) // hide
ct.setActiveView(ct.tableviewname)
ct.intervalFetchData()
}
@ -147,6 +171,10 @@ func (ct *Cointop) setActiveView(v string) error {
} else if v == ct.tableviewname {
ct.g.SetViewOnTop(ct.statusbarviewname)
}
if v == ct.portfolioupdatemenuviewname {
ct.g.SetViewOnTop(ct.inputviewname)
ct.g.SetCurrentView(ct.inputviewname)
}
return nil
}

@ -212,6 +212,9 @@ func (ct *Cointop) navigatePageLastLine() error {
}
func (ct *Cointop) prevPage() error {
if ct.isFirstPage() {
return nil
}
ct.setPage(ct.page - 1)
ct.updateTable()
ct.rowChanged()
@ -232,6 +235,10 @@ func (ct *Cointop) firstPage() error {
return nil
}
func (ct *Cointop) isFirstPage() bool {
return ct.page == 0
}
func (ct *Cointop) lastPage() error {
ct.page = ct.getListCount() / ct.perpage
ct.updateTable()

@ -1,5 +1,15 @@
package cointop
import (
"fmt"
"regexp"
"strconv"
"strings"
"github.com/miguelmota/cointop/pkg/color"
"github.com/miguelmota/cointop/pkg/pad"
)
func (ct *Cointop) togglePortfolio() error {
ct.filterByFavorites = false
ct.portfoliovisible = !ct.portfoliovisible
@ -13,3 +23,159 @@ func (ct *Cointop) toggleShowPortfolio() error {
ct.updateTable()
return nil
}
func (ct *Cointop) togglePortfolioUpdateMenu() error {
ct.portfolioupdatemenuvisible = !ct.portfolioupdatemenuvisible
if ct.portfolioupdatemenuvisible {
return ct.showPortfolioUpdateMenu()
}
return ct.hidePortfolioUpdateMenu()
}
func (ct *Cointop) updatePortfolioUpdateMenu() {
coin := ct.highlightedRowCoin()
exists := ct.portfolioEntryExists(coin)
value := strconv.FormatFloat(coin.Holdings, 'f', -1, 64)
var mode string
var current string
var submitText string
if exists {
mode = "Edit"
current = fmt.Sprintf("(current %s %s)", value, coin.Symbol)
submitText = "Set"
} else {
mode = "Add"
submitText = "Add"
}
header := color.GreenBg(fmt.Sprintf(" %s Portfolio Entry %s\n\n", mode, pad.Left("[q] close ", ct.maxtablewidth-15, " ")))
label := fmt.Sprintf(" Enter holdings for %s %s", color.Yellow(coin.Name), current)
content := fmt.Sprintf("%s\n%s\n\n%s%s\n\n\n [Enter] %s [ESC] Cancel", header, label, strings.Repeat(" ", 29), coin.Symbol, submitText)
ct.update(func() {
ct.portfolioupdatemenuview.Clear()
ct.portfolioupdatemenuview.Frame = true
fmt.Fprintln(ct.portfolioupdatemenuview, content)
fmt.Fprintln(ct.inputview, value)
ct.inputview.SetCursor(len(value), 0)
})
}
func (ct *Cointop) showPortfolioUpdateMenu() error {
ct.portfolioupdatemenuvisible = true
ct.updatePortfolioUpdateMenu()
ct.setActiveView(ct.portfolioupdatemenuviewname)
return nil
}
func (ct *Cointop) hidePortfolioUpdateMenu() error {
ct.portfolioupdatemenuvisible = false
ct.setViewOnBottom(ct.portfolioupdatemenuviewname)
ct.setViewOnBottom(ct.inputviewname)
ct.setActiveView(ct.tableviewname)
ct.update(func() {
ct.portfolioupdatemenuview.Clear()
ct.portfolioupdatemenuview.Frame = false
fmt.Fprintln(ct.portfolioupdatemenuview, "")
ct.inputview.Clear()
fmt.Fprintln(ct.inputview, "")
})
return nil
}
// sets portfolio entry holdings from inputed value
func (ct *Cointop) setPortfolioHoldings() error {
defer ct.hidePortfolioUpdateMenu()
coin := ct.highlightedRowCoin()
b := make([]byte, 100)
n, err := ct.inputview.Read(b)
if n == 0 {
return nil
}
value := normalizeFloatstring(string(b))
shouldDelete := value == ""
var holdings float64
if !shouldDelete {
holdings, err = strconv.ParseFloat(value, 64)
if err != nil {
return err
}
}
ct.setPortfolioEntry(coin.Name, holdings)
if shouldDelete {
ct.removePortfolioEntry(coin.Name)
ct.updateTable()
ct.goToGlobalIndex(0)
} else {
ct.updateTable()
ct.goToGlobalIndex(coin.Rank - 1)
}
return nil
}
func (ct *Cointop) portfolioEntry(c *coin) (*portfolioEntry, bool) {
if c == nil {
return &portfolioEntry{}, true
}
var p *portfolioEntry
var isNew bool
var ok bool
key := strings.ToLower(c.Name)
if p, ok = ct.portfolio.Entries[key]; !ok {
// NOTE: if not found then try the symbol
key := strings.ToLower(c.Symbol)
if p, ok = ct.portfolio.Entries[key]; !ok {
p = &portfolioEntry{
Coin: c.Name,
Holdings: 0,
}
isNew = true
}
}
return p, isNew
}
func (ct *Cointop) setPortfolioEntry(coin string, holdings float64) {
c, _ := ct.allcoinsslugmap[strings.ToLower(coin)]
p, isNew := ct.portfolioEntry(c)
if isNew {
key := strings.ToLower(coin)
ct.portfolio.Entries[key] = &portfolioEntry{
Coin: coin,
Holdings: holdings,
}
} else {
p.Holdings = holdings
}
}
func (ct *Cointop) removePortfolioEntry(coin string) {
delete(ct.portfolio.Entries, strings.ToLower(coin))
}
func (ct *Cointop) portfolioEntryExists(c *coin) bool {
_, isNew := ct.portfolioEntry(c)
return !isNew
}
func (ct *Cointop) portfolioEntriesCount() int {
return len(ct.portfolio.Entries)
}
func normalizeFloatstring(input string) string {
re := regexp.MustCompile(`(\d+\.\d+|\.\d+|\d+)`)
result := re.FindStringSubmatch(input)
if len(result) > 0 {
return result[0]
}
return ""
}

@ -38,6 +38,8 @@ func defaultShortcuts() map[string]string {
"b": "sort_column_balance",
"c": "show_currency_convert_menu",
"C": "show_currency_convert_menu",
"e": "show_portfolio_edit_menu",
"E": "show_portfolio_edit_menu",
"f": "toggle_favorite",
"F": "toggle_show_favorites",
"g": "move_to_page_first_row",

@ -54,7 +54,7 @@ func (ct *Cointop) refreshTable() error {
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))),
color.White(fmt.Sprintf("%15s", strconv.FormatFloat(coin.Holdings, 'f', -1, 64))),
colorbalance(fmt.Sprintf("%15s", humanize.Commaf(coin.Balance))),
color24h(fmt.Sprintf("%8.2f%%", coin.PercentChange24H)),
color.White(pad.Right(fmt.Sprintf("%17s", lastUpdated), 80, " ")),
@ -170,24 +170,15 @@ func (ct *Cointop) updateTable() error {
if ct.portfoliovisible {
for i := range ct.allcoins {
if len(ct.portfolio.Entries) == 0 {
if ct.portfolioEntriesCount() == 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
}
p, isNew := ct.portfolioEntry(coin)
if isNew {
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
coin.Holdings = p.Holdings
balance := coin.Price * p.Holdings
balancestr := fmt.Sprintf("%.2f", balance)
@ -298,6 +289,17 @@ func (ct *Cointop) allCoins() []*coin {
return list
}
if ct.portfoliovisible {
var list []*coin
for i := range ct.allcoins {
coin := ct.allcoins[i]
if ct.portfolioEntryExists(coin) {
list = append(list, coin)
}
}
return list
}
return ct.allcoins
}

@ -11,4 +11,5 @@ func NewCMC() Interface {
// NewCC new CryptoCompare API
func NewCC() {
// TODO
}

Loading…
Cancel
Save