Add price alerts table

pull/94/head
Miguel Mota 3 years ago
parent cba53fe5d4
commit 3bc93134c9

@ -10,9 +10,5 @@ func (ct *Cointop) SetActiveView(v string) error {
} else if v == ct.Views.Table.Name() {
ct.g.SetViewOnTop(ct.Views.Statusbar.Name())
}
if v == ct.Views.PortfolioUpdateMenu.Name() {
ct.g.SetViewOnTop(ct.Views.Input.Name())
ct.g.SetCurrentView(ct.Views.Input.Name())
}
return nil
}

@ -26,16 +26,14 @@ var ErrInvalidAPIChoice = errors.New("Invalid API choice")
// Views are all views in cointop
type Views struct {
Chart *ChartView
Table *TableView
TableHeader *TableHeaderView
Marketbar *MarketbarView
SearchField *SearchFieldView
Statusbar *StatusbarView
Help *HelpView
ConvertMenu *ConvertMenuView
Input *InputView
PortfolioUpdateMenu *PortfolioUpdateMenuView
Chart *ChartView
Table *TableView
TableHeader *TableHeaderView
Marketbar *MarketbarView
SearchField *SearchFieldView
Statusbar *StatusbarView
Menu *MenuView
Input *InputView
}
// State is the state preferences of cointop
@ -76,6 +74,7 @@ type State struct {
onlyTable bool
chartHeight int
priceAlerts *PriceAlerts
priceAlertEditID string
}
// Cointop cointop
@ -124,7 +123,7 @@ type PriceAlert struct {
ID string
CoinName string
TargetPrice float64
Direction string
Operator string
Frequency string
CreatedAt string
Expired bool
@ -238,16 +237,14 @@ func NewCointop(config *Config) (*Cointop, error) {
},
TableColumnOrder: TableColumnOrder(),
Views: &Views{
Chart: NewChartView(),
Table: NewTableView(),
TableHeader: NewTableHeaderView(),
Marketbar: NewMarketbarView(),
SearchField: NewSearchFieldView(),
Statusbar: NewStatusbarView(),
Help: NewHelpView(),
ConvertMenu: NewConvertMenuView(),
Input: NewInputView(),
PortfolioUpdateMenu: NewPortfolioUpdateMenuView(),
Chart: NewChartView(),
Table: NewTableView(),
TableHeader: NewTableHeaderView(),
Marketbar: NewMarketbarView(),
SearchField: NewSearchFieldView(),
Statusbar: NewStatusbarView(),
Menu: NewMenuView(),
Input: NewInputView(),
},
}

@ -243,14 +243,17 @@ func (ct *Cointop) configToToml() ([]byte, error) {
var apiChoiceIfc interface{} = ct.apiChoice
priceAlertsIfc := make([]interface{}, len(ct.State.priceAlerts.Entries))
for i, priceAlert := range ct.State.priceAlerts.Entries {
priceAlertsIfc[i] = []string{
var priceAlertsIfc []interface{}
for _, priceAlert := range ct.State.priceAlerts.Entries {
if priceAlert.Expired {
continue
}
priceAlertsIfc = append(priceAlertsIfc, []string{
priceAlert.CoinName,
priceAlert.Direction,
priceAlert.Operator,
strconv.FormatFloat(priceAlert.TargetPrice, 'f', -1, 64),
priceAlert.Frequency,
}
})
}
priceAlertsMapIfc := map[string]interface{}{
"alerts": priceAlertsIfc,
@ -480,11 +483,11 @@ func (ct *Cointop) loadPriceAlertsFromConfig() error {
if !ok {
return ErrInvalidPriceAlert
}
direction, ok := priceAlert[1].(string)
operator, ok := priceAlert[1].(string)
if !ok {
return ErrInvalidPriceAlert
}
if _, ok := PriceAlertDirectionsMap[direction]; !ok {
if _, ok := PriceAlertOperatorMap[operator]; !ok {
return ErrInvalidPriceAlert
}
targetPriceStr, ok := priceAlert[2].(string)
@ -502,11 +505,11 @@ func (ct *Cointop) loadPriceAlertsFromConfig() error {
if _, ok := PriceAlertFrequencyMap[frequency]; !ok {
return ErrInvalidPriceAlert
}
id := strings.ToLower(fmt.Sprintf("%s_%s_%v_%s", coinName, direction, targetPrice, frequency))
id := strings.ToLower(fmt.Sprintf("%s_%s_%v_%s", coinName, operator, targetPrice, frequency))
entry := &PriceAlert{
ID: id,
CoinName: coinName,
Direction: direction,
Operator: operator,
TargetPrice: targetPrice,
Frequency: frequency,
}

@ -15,5 +15,5 @@ const CoinsView = "coins"
// FavoritesView is favorites table constant
const FavoritesView = "favorites"
// AlertsView is alerts table constant
const AlertsView = "alerts"
// PriceAlertsView is price alerts table constant
const PriceAlertsView = "price_alerts"

@ -8,7 +8,6 @@ import (
color "github.com/miguelmota/cointop/pkg/color"
"github.com/miguelmota/cointop/pkg/pad"
"github.com/miguelmota/cointop/pkg/ui"
)
// FiatCurrencyNames is a mpa of currency symbols to names.
@ -105,15 +104,6 @@ var CurrencySymbolMap = map[string]string{
var alphanumericcharacters = []rune{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'}
// ConvertMenuView is structure for convert menu view
type ConvertMenuView = ui.View
// NewConvertMenuView returns a new convert menu view
func NewConvertMenuView() *ConvertMenuView {
var view *ConvertMenuView = ui.NewView("convertmenu")
return view
}
// IsSupportedCurrencyConversion returns true if it's a supported currency conversion
func (ct *Cointop) IsSupportedCurrencyConversion(convert string) bool {
conversions := ct.SupportedCurrencyConversions()
@ -160,10 +150,10 @@ func (ct *Cointop) SortedSupportedCurrencyConversions() []string {
// UpdateConvertMenu updates the convert menu
func (ct *Cointop) UpdateConvertMenu() error {
ct.debuglog("updateConvertMenu()")
header := ct.colorscheme.MenuHeader(fmt.Sprintf(" Currency Conversion %s\n\n", pad.Left("[q] close menu ", ct.maxTableWidth-20, " ")))
header := ct.colorscheme.MenuHeader(fmt.Sprintf(" Currency Conversion %s\n\n", pad.Left("[q] close ", ct.maxTableWidth-24, " ")))
helpline := " Press the corresponding key to select currency for conversion\n\n"
cnt := 0
h := ct.Views.ConvertMenu.Height()
h := ct.Views.Menu.Height()
percol := h - 5
cols := make([][]string, percol)
for i := range cols {
@ -205,8 +195,8 @@ func (ct *Cointop) UpdateConvertMenu() error {
content := fmt.Sprintf("%s%s%s", header, helpline, body)
ct.UpdateUI(func() error {
ct.Views.ConvertMenu.SetFrame(true)
return ct.Views.ConvertMenu.Update(content)
ct.Views.Menu.SetFrame(true)
return ct.Views.Menu.Update(content)
})
return nil
@ -262,7 +252,7 @@ func (ct *Cointop) ShowConvertMenu() error {
ct.debuglog("showConvertMenu()")
ct.State.convertMenuVisible = true
ct.UpdateConvertMenu()
ct.SetActiveView(ct.Views.ConvertMenu.Name())
ct.SetActiveView(ct.Views.Menu.Name())
return nil
}
@ -270,12 +260,11 @@ func (ct *Cointop) ShowConvertMenu() error {
func (ct *Cointop) HideConvertMenu() error {
ct.debuglog("hideConvertMenu()")
ct.State.convertMenuVisible = false
ct.ui.SetViewOnBottom(ct.Views.ConvertMenu)
ct.ui.SetViewOnBottom(ct.Views.Menu)
ct.SetActiveView(ct.Views.Table.Name())
ct.UpdateUI(func() error {
ct.Views.ConvertMenu.SetFrame(false)
return ct.Views.ConvertMenu.Update("")
return nil
ct.Views.Menu.SetFrame(false)
return ct.Views.Menu.Update("")
})
return nil
}

@ -44,7 +44,7 @@ func DefaultShortcuts() map[string]string {
"C": "show_currency_convert_menu",
"e": "show_portfolio_edit_menu",
"E": "show_portfolio_edit_menu",
"A": "toggle_alerts",
"A": "toggle_price_alerts",
"f": "toggle_favorite",
"F": "toggle_show_favorites",
"g": "move_to_page_first_row",
@ -79,6 +79,7 @@ func DefaultShortcuts() map[string]string {
"{": "first_chart_range",
">": "scroll_right",
"<": "scroll_left",
"+": "show_price_alert_add_menu",
"\\\\": "toggle_table_fullscreen",
}
}

@ -5,18 +5,8 @@ import (
"sort"
"github.com/miguelmota/cointop/pkg/pad"
"github.com/miguelmota/cointop/pkg/ui"
)
// HelpView is structure for help view
type HelpView = ui.View
// NewHelpView returns a new help view
func NewHelpView() *HelpView {
var view *HelpView = ui.NewView("help")
return view
}
// UpdateHelp updates the help views
func (ct *Cointop) UpdateHelp() {
ct.debuglog("updateHelp()")
@ -28,7 +18,7 @@ func (ct *Cointop) UpdateHelp() {
header := ct.colorscheme.MenuHeader(fmt.Sprintf(" Help %s\n\n", pad.Left("[q] close ", ct.maxTableWidth-10, " ")))
cnt := 0
h := ct.Views.Help.Height()
h := ct.Views.Menu.Height()
percol := h - 11
cols := make([][]string, percol)
for i := range cols {
@ -61,8 +51,8 @@ func (ct *Cointop) UpdateHelp() {
content := fmt.Sprintf("%s %s\n %s\n\n %s\n\n%s\n %s", header, versionLine, licenseLine, instructionsLine, body, infoLine)
ct.UpdateUI(func() error {
ct.Views.Help.SetFrame(true)
return ct.Views.Help.Update(content)
ct.Views.Menu.SetFrame(true)
return ct.Views.Menu.Update(content)
})
}
@ -71,7 +61,7 @@ func (ct *Cointop) ShowHelp() error {
ct.debuglog("showHelp()")
ct.State.helpVisible = true
ct.UpdateHelp()
ct.SetActiveView(ct.Views.Help.Name())
ct.SetActiveView(ct.Views.Menu.Name())
return nil
}
@ -79,11 +69,11 @@ func (ct *Cointop) ShowHelp() error {
func (ct *Cointop) HideHelp() error {
ct.debuglog("hideHelp()")
ct.State.helpVisible = false
ct.ui.SetViewOnBottom(ct.Views.Help)
ct.ui.SetViewOnBottom(ct.Views.Menu)
ct.SetActiveView(ct.Views.Table.Name())
ct.UpdateUI(func() error {
ct.Views.Help.SetFrame(false)
return ct.Views.Help.Update("")
ct.Views.Menu.SetFrame(false)
return ct.Views.Menu.Update("")
})
return nil
}

@ -306,8 +306,8 @@ func (ct *Cointop) Keybindings(g *gocui.Gui) error {
case "open_search":
fn = ct.Keyfn(ct.openSearch)
view = ""
case "toggle_alerts":
fn = ct.Keyfn(ct.ToggleAlerts)
case "toggle_price_alerts":
fn = ct.Keyfn(ct.TogglePriceAlerts)
case "toggle_favorite":
fn = ct.Keyfn(ct.ToggleFavorite)
case "toggle_favorites":
@ -342,6 +342,10 @@ func (ct *Cointop) Keybindings(g *gocui.Gui) error {
fn = ct.Keyfn(ct.ToggleShowPortfolio)
case "show_portfolio_edit_menu":
fn = ct.Keyfn(ct.TogglePortfolioUpdateMenu)
case "show_price_alert_edit_menu":
fn = ct.Keyfn(ct.ShowPriceAlertsUpdateMenu)
case "show_price_alert_add_menu":
fn = ct.Keyfn(ct.ShowPriceAlertsAddMenu)
case "toggle_table_fullscreen":
fn = ct.Keyfn(ct.ToggleTableFullscreen)
view = ""
@ -369,19 +373,19 @@ func (ct *Cointop) Keybindings(g *gocui.Gui) error {
ct.SetKeybindingMod(gocui.KeyEsc, gocui.ModNone, ct.Keyfn(ct.CancelSearch), ct.Views.SearchField.Name())
// keys to quit help when open
ct.SetKeybindingMod(gocui.KeyEsc, gocui.ModNone, ct.Keyfn(ct.HideHelp), ct.Views.Help.Name())
ct.SetKeybindingMod('q', gocui.ModNone, ct.Keyfn(ct.HideHelp), ct.Views.Help.Name())
ct.SetKeybindingMod(gocui.KeyEsc, gocui.ModNone, ct.Keyfn(ct.HideHelp), ct.Views.Menu.Name())
ct.SetKeybindingMod('q', gocui.ModNone, ct.Keyfn(ct.HideHelp), ct.Views.Menu.Name())
// keys to quit portfolio update menu when open
ct.SetKeybindingMod(gocui.KeyEsc, gocui.ModNone, ct.Keyfn(ct.HidePortfolioUpdateMenu), ct.Views.Input.Name())
ct.SetKeybindingMod('q', gocui.ModNone, ct.Keyfn(ct.HidePortfolioUpdateMenu), ct.Views.Input.Name())
// keys to update portfolio holdings
ct.SetKeybindingMod(gocui.KeyEnter, gocui.ModNone, ct.Keyfn(ct.SetPortfolioHoldings), ct.Views.Input.Name())
// keys to quit convert menu when open
ct.SetKeybindingMod(gocui.KeyEsc, gocui.ModNone, ct.Keyfn(ct.HideConvertMenu), ct.Views.ConvertMenu.Name())
ct.SetKeybindingMod('q', gocui.ModNone, ct.Keyfn(ct.HideConvertMenu), ct.Views.ConvertMenu.Name())
ct.SetKeybindingMod(gocui.KeyEsc, gocui.ModNone, ct.Keyfn(ct.HideConvertMenu), ct.Views.Menu.Name())
ct.SetKeybindingMod('q', gocui.ModNone, ct.Keyfn(ct.HideConvertMenu), ct.Views.Menu.Name())
// keys to update portfolio holdings
ct.SetKeybindingMod(gocui.KeyEnter, gocui.ModNone, ct.Keyfn(ct.EnterKeyPressHandler), ct.Views.Input.Name())
// mouse events
ct.SetKeybindingMod(gocui.MouseRelease, gocui.ModNone, ct.Keyfn(ct.MouseRelease), "")
@ -395,7 +399,7 @@ func (ct *Cointop) Keybindings(g *gocui.Gui) error {
// TODO: use scrolling table
keys := ct.SortedSupportedCurrencyConversions()
for i, k := range keys {
ct.SetKeybindingMod(rune(alphanumericcharacters[i]), gocui.ModNone, ct.Keyfn(ct.SetCurrencyConverstionFn(k)), ct.Views.ConvertMenu.Name())
ct.SetKeybindingMod(rune(alphanumericcharacters[i]), gocui.ModNone, ct.Keyfn(ct.SetCurrencyConverstionFn(k)), ct.Views.Menu.Name())
}
return nil

@ -139,16 +139,10 @@ func (ct *Cointop) layout() error {
ct.Views.SearchField.SetBgColor(ct.colorscheme.gocuiBgColor("searchbar"))
}
if err := ct.ui.SetView(ct.Views.Help, 1, 1, ct.maxTableWidth-1, maxY-1); err != nil {
ct.Views.Help.SetFrame(false)
ct.Views.Help.SetFgColor(ct.colorscheme.gocuiFgColor("menu"))
ct.Views.Help.SetBgColor(ct.colorscheme.gocuiBgColor("menu"))
}
if err := ct.ui.SetView(ct.Views.PortfolioUpdateMenu, 1, 1, ct.maxTableWidth-1, maxY-1); err != nil {
ct.Views.PortfolioUpdateMenu.SetFrame(false)
ct.Views.PortfolioUpdateMenu.SetFgColor(ct.colorscheme.gocuiFgColor("menu"))
ct.Views.PortfolioUpdateMenu.SetBgColor(ct.colorscheme.gocuiBgColor("menu"))
if err := ct.ui.SetView(ct.Views.Menu, 1, 1, ct.maxTableWidth-1, maxY-1); err != nil {
ct.Views.Menu.SetFrame(false)
ct.Views.Menu.SetFgColor(ct.colorscheme.gocuiFgColor("menu"))
ct.Views.Menu.SetBgColor(ct.colorscheme.gocuiBgColor("menu"))
}
if err := ct.ui.SetView(ct.Views.Input, 3, 6, 30, 8); err != nil {
@ -157,20 +151,12 @@ func (ct *Cointop) layout() error {
ct.Views.Input.SetWrap(true)
ct.Views.Input.SetFgColor(ct.colorscheme.gocuiFgColor("menu"))
ct.Views.Input.SetBgColor(ct.colorscheme.gocuiBgColor("menu"))
}
if err := ct.ui.SetView(ct.Views.ConvertMenu, 1, 1, ct.maxTableWidth-1, maxY-1); err != nil {
ct.Views.ConvertMenu.SetFrame(false)
ct.Views.ConvertMenu.SetFgColor(ct.colorscheme.gocuiFgColor("menu"))
ct.Views.ConvertMenu.SetBgColor(ct.colorscheme.gocuiBgColor("menu"))
// run only once on init.
// this bit of code should be at the bottom
ct.ui.SetViewOnBottom(ct.Views.SearchField) // hide
ct.ui.SetViewOnBottom(ct.Views.Help) // hide
ct.ui.SetViewOnBottom(ct.Views.ConvertMenu) // hide
ct.ui.SetViewOnBottom(ct.Views.PortfolioUpdateMenu) // hide
ct.ui.SetViewOnBottom(ct.Views.Input) // hide
ct.ui.SetViewOnBottom(ct.Views.SearchField) // hide
ct.ui.SetViewOnBottom(ct.Views.Input) // hide
ct.ui.SetViewOnBottom(ct.Views.Menu) // hide
ct.SetActiveView(ct.Views.Table.Name())
ct.intervalFetchData()
}

@ -0,0 +1,18 @@
package cointop
import "github.com/miguelmota/cointop/pkg/ui"
// MenuView is structure for menu view
type MenuView = ui.View
// NewMenuView returns a new menu view
func NewMenuView() *MenuView {
var view *MenuView = ui.NewView("menu")
return view
}
// HideMenu hides the menu view
func (ct *Cointop) HideMenu() error {
ct.debuglog("hideMenu()")
return nil
}

@ -95,7 +95,7 @@ func (ct *Cointop) PageDown() error {
cx := ct.Views.Table.CursorX() // relative cursor position
sy := ct.Views.Table.Height() // rows in visible view
k := oy + sy
l := len(ct.State.coins)
l := ct.TableRowsLen()
// end of table
if (oy + sy + sy) > l {
k = l - sy
@ -180,7 +180,7 @@ func (ct *Cointop) NavigateLastLine() error {
ox := ct.Views.Table.OriginX()
cx := ct.Views.Table.CursorX()
sy := ct.Views.Table.Height()
l := len(ct.State.coins)
l := ct.TableRowsLen()
k := l - sy
if err := ct.Views.Table.SetOrigin(ox, k); err != nil {
return err
@ -337,7 +337,7 @@ func (ct *Cointop) IsLastRow() bool {
ct.debuglog("isLastRow()")
oy := ct.Views.Table.OriginY()
cy := ct.Views.Table.CursorY()
numRows := len(ct.State.coins) - 1
numRows := ct.TableRowsLen() - 1
return (cy + oy + 1) > numRows
}
@ -516,3 +516,13 @@ func (ct *Cointop) MouseWheelUp() error {
func (ct *Cointop) MouseWheelDown() error {
return nil
}
// TableRowsLen returns the number of table row entries
func (ct *Cointop) TableRowsLen() int {
ct.debuglog("TableRowsLen()")
if ct.IsPriceAlertsVisible() {
return ct.ActivePriceAlertsLen()
}
return len(ct.State.coins)
}

@ -16,18 +16,8 @@ import (
"github.com/miguelmota/cointop/pkg/humanize"
"github.com/miguelmota/cointop/pkg/pad"
"github.com/miguelmota/cointop/pkg/table"
"github.com/miguelmota/cointop/pkg/ui"
)
// PortfolioUpdateMenuView is structure for portfolio update menu view
type PortfolioUpdateMenuView = ui.View
// NewPortfolioUpdateMenuView returns a new portfolio update menu view
func NewPortfolioUpdateMenuView() *PortfolioUpdateMenuView {
var view *PortfolioUpdateMenuView = ui.NewView("portfolioupdatemenu")
return view
}
// GetPortfolioTableHeaders returns the portfolio table headers
func (ct *Cointop) GetPortfolioTableHeaders() []string {
return []string{
@ -214,8 +204,8 @@ func (ct *Cointop) UpdatePortfolioUpdateMenu() error {
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.UpdateUI(func() error {
ct.Views.PortfolioUpdateMenu.SetFrame(true)
ct.Views.PortfolioUpdateMenu.Update(content)
ct.Views.Menu.SetFrame(true)
ct.Views.Menu.Update(content)
ct.Views.Input.Write(value)
ct.Views.Input.SetCursor(len(value), 0)
return nil
@ -226,6 +216,12 @@ func (ct *Cointop) UpdatePortfolioUpdateMenu() error {
// ShowPortfolioUpdateMenu shows the portfolio update menu
func (ct *Cointop) ShowPortfolioUpdateMenu() error {
ct.debuglog("showPortfolioUpdateMenu()")
// TODO: separation of concerns
if ct.IsPriceAlertsVisible() {
return ct.ShowPriceAlertsUpdateMenu()
}
coin := ct.HighlightedRowCoin()
if coin == nil {
ct.TogglePortfolio()
@ -235,7 +231,10 @@ func (ct *Cointop) ShowPortfolioUpdateMenu() error {
ct.State.lastSelectedRowIndex = ct.HighlightedPageRowIndex()
ct.State.portfolioUpdateMenuVisible = true
ct.UpdatePortfolioUpdateMenu()
ct.SetActiveView(ct.Views.PortfolioUpdateMenu.Name())
ct.ui.SetCursor(true)
ct.SetActiveView(ct.Views.Menu.Name())
ct.g.SetViewOnTop(ct.Views.Input.Name())
ct.g.SetCurrentView(ct.Views.Input.Name())
return nil
}
@ -243,12 +242,13 @@ func (ct *Cointop) ShowPortfolioUpdateMenu() error {
func (ct *Cointop) HidePortfolioUpdateMenu() error {
ct.debuglog("hidePortfolioUpdateMenu()")
ct.State.portfolioUpdateMenuVisible = false
ct.ui.SetViewOnBottom(ct.Views.PortfolioUpdateMenu)
ct.ui.SetViewOnBottom(ct.Views.Menu)
ct.ui.SetViewOnBottom(ct.Views.Input)
ct.ui.SetCursor(false)
ct.SetActiveView(ct.Views.Table.Name())
ct.UpdateUI(func() error {
ct.Views.PortfolioUpdateMenu.SetFrame(false)
ct.Views.PortfolioUpdateMenu.Update("")
ct.Views.Menu.SetFrame(false)
ct.Views.Menu.Update("")
ct.Views.Input.Update("")
return nil
})
@ -295,6 +295,10 @@ func (ct *Cointop) SetPortfolioHoldings() error {
ct.GoToPageRowIndex(ct.State.lastSelectedRowIndex)
}
if err := ct.Save(); err != nil {
return err
}
return nil
}

@ -1,39 +1,38 @@
package cointop
import (
"errors"
"fmt"
"log"
"regexp"
"strconv"
"strings"
"time"
"github.com/miguelmota/cointop/pkg/humanize"
"github.com/miguelmota/cointop/pkg/notifier"
"github.com/miguelmota/cointop/pkg/pad"
"github.com/miguelmota/cointop/pkg/table"
)
// GetAlertsTableHeaders returns the alerts table headers
func (ct *Cointop) GetAlertsTableHeaders() []string {
// GetPriceAlertsTableHeaders returns the alerts table headers
func (ct *Cointop) GetPriceAlertsTableHeaders() []string {
return []string{
"name",
"symbol",
"targetprice", //>600
"targetprice",
"price",
"frequency",
}
}
var gt = ">"
var gte = "≥"
var lte = "≤"
var lt = "<"
var eq = "="
// PriceAlertDirectionsMap is map of valid price alert direction symbols
var PriceAlertDirectionsMap = map[string]bool{
">": true,
"<": true,
">=": true,
"<=": true,
"=": true,
// PriceAlertOperatorMap is map of valid price alert operator symbols
var PriceAlertOperatorMap = map[string]string{
">": ">",
"<": "<",
">=": "≥",
"<=": "≤",
"=": "=",
}
// PriceAlertFrequencyMap is map of valid price alert frequency values
@ -42,12 +41,16 @@ var PriceAlertFrequencyMap = map[string]bool{
"reoccurring": true,
}
// GetAlertsTable returns the table for displaying alerts
func (ct *Cointop) GetAlertsTable() *table.Table {
// GetPriceAlertsTable returns the table for displaying alerts
func (ct *Cointop) GetPriceAlertsTable() *table.Table {
ct.debuglog("getPriceAlertsTable()")
maxX := ct.width()
t := table.NewTable().SetWidth(maxX)
for _, entry := range ct.State.priceAlerts.Entries {
if entry.Expired {
continue
}
ifc, ok := ct.State.allCoinsSlugMap.Load(entry.CoinName)
if !ok {
continue
@ -60,7 +63,11 @@ func (ct *Cointop) GetAlertsTable() *table.Table {
symbol := TruncateString(coin.Symbol, 6)
namecolor := ct.colorscheme.TableRow
frequency := entry.Frequency
targetPrice := fmt.Sprintf("%s%v", gte, entry.TargetPrice)
_, ok = PriceAlertOperatorMap[entry.Operator]
if !ok {
continue
}
targetPrice := fmt.Sprintf("%s %s", entry.Operator, humanize.Commaf(entry.TargetPrice))
t.AddRowCells(
&table.RowCell{
@ -81,7 +88,7 @@ func (ct *Cointop) GetAlertsTable() *table.Table {
LeftMargin: 1,
Width: 16,
LeftAlign: false,
Color: ct.colorscheme.TableRow,
Color: ct.colorscheme.TableColumnPrice,
Text: targetPrice,
},
&table.RowCell{
@ -104,17 +111,18 @@ func (ct *Cointop) GetAlertsTable() *table.Table {
return t
}
// ToggleAlerts toggles the alerts view
func (ct *Cointop) ToggleAlerts() error {
ct.debuglog("toggleAlerts()")
ct.ToggleSelectedView(AlertsView)
// TogglePriceAlerts toggles the price alerts view
func (ct *Cointop) TogglePriceAlerts() error {
ct.debuglog("togglePriceAlerts()")
ct.ToggleSelectedView(PriceAlertsView)
ct.NavigateFirstLine()
go ct.UpdateTable()
return nil
}
// IsAlertsVisible returns true if alerts view is visible
func (ct *Cointop) IsAlertsVisible() bool {
return ct.State.selectedView == AlertsView
// IsPriceAlertsVisible returns true if alerts view is visible
func (ct *Cointop) IsPriceAlertsVisible() bool {
return ct.State.selectedView == PriceAlertsView
}
// PriceAlertWatcher starts the price alert watcher
@ -142,18 +150,6 @@ func (ct *Cointop) CheckPriceAlert(alert *PriceAlert) error {
return nil
}
cacheKey := ct.CacheKey("priceAlerts")
var cachedEntries []*PriceAlert
ct.filecache.Get(cacheKey, &cachedEntries)
for _, cachedEntry := range cachedEntries {
if cachedEntry.ID == alert.ID {
alert.Expired = cachedEntry.Expired
if alert.Expired {
return nil
}
}
}
coinIfc, _ := ct.State.allCoinsSlugMap.Load(alert.CoinName)
coin, ok := coinIfc.(*Coin)
if !ok {
@ -161,24 +157,24 @@ func (ct *Cointop) CheckPriceAlert(alert *PriceAlert) error {
}
var msg string
title := "Cointop Alert"
priceStr := fmt.Sprintf("$%s", humanize.Commaf(alert.TargetPrice))
if alert.Direction == ">" {
priceStr := fmt.Sprintf("%s%s (%s%s)", ct.CurrencySymbol(), humanize.Commaf(alert.TargetPrice), ct.CurrencySymbol(), humanize.Commaf(coin.Price))
if alert.Operator == ">" {
if coin.Price > alert.TargetPrice {
msg = fmt.Sprintf("%s price is greater than %v", alert.CoinName, priceStr)
}
} else if alert.Direction == ">=" {
} else if alert.Operator == ">=" {
if coin.Price >= alert.TargetPrice {
msg = fmt.Sprintf("%s price is greater than or equal to %v", alert.CoinName, priceStr)
}
} else if alert.Direction == "<" {
} else if alert.Operator == "<" {
if coin.Price < alert.TargetPrice {
msg = fmt.Sprintf("%s price is less than %v", alert.CoinName, priceStr)
}
} else if alert.Direction == "<=" {
} else if alert.Operator == "<=" {
if coin.Price <= alert.TargetPrice {
msg = fmt.Sprintf("%s price is less than or equal to %v", alert.CoinName, priceStr)
}
} else if alert.Direction == "=" {
} else if alert.Operator == "=" {
if coin.Price == alert.TargetPrice {
msg = fmt.Sprintf("%s price is equal to %v", alert.CoinName, priceStr)
}
@ -192,7 +188,244 @@ func (ct *Cointop) CheckPriceAlert(alert *PriceAlert) error {
}
alert.Expired = true
ct.filecache.Set(cacheKey, ct.State.priceAlerts.Entries, 87600*time.Hour)
}
if err := ct.Save(); err != nil {
return err
}
return nil
}
// UpdatePriceAlertsUpdateMenu updates the alerts update menu view
func (ct *Cointop) UpdatePriceAlertsUpdateMenu(isNew bool) error {
ct.debuglog("updatePriceAlertsUpdateMenu()")
exists := false
var value string
var currentPrice string
var coinName string
ct.State.priceAlertEditID = ""
if !isNew && ct.IsPriceAlertsVisible() {
rowIndex := ct.HighlightedRowIndex()
entry := ct.State.priceAlerts.Entries[rowIndex]
ifc, ok := ct.State.allCoinsSlugMap.Load(entry.CoinName)
if ok {
coin, ok := ifc.(*Coin)
if ok {
coinName = entry.CoinName
currentPrice = strconv.FormatFloat(coin.Price, 'f', -1, 64)
value = fmt.Sprintf("%s %v", entry.Operator, entry.TargetPrice)
ct.State.priceAlertEditID = entry.ID
exists = true
}
}
}
var mode string
var current string
var submitText string
if exists {
mode = "Edit"
current = fmt.Sprintf("(current %s%s)", ct.CurrencySymbol(), currentPrice)
submitText = "Set"
} else {
coin := ct.HighlightedRowCoin()
coinName = coin.Name
currentPrice = strconv.FormatFloat(coin.Price, 'f', -1, 64)
value = fmt.Sprintf("> %s", currentPrice)
mode = "Create"
submitText = "Create"
}
header := ct.colorscheme.MenuHeader(fmt.Sprintf(" %s Alert Entry %s\n\n", mode, pad.Left("[q] close ", ct.maxTableWidth-26, " ")))
label := fmt.Sprintf(" Enter target price for %s %s", ct.colorscheme.MenuLabel(coinName), current)
content := fmt.Sprintf("%s\n%s\n\n%s%s\n\n\n [Enter] %s [ESC] Cancel", header, label, strings.Repeat(" ", 29), ct.State.currencyConversion, submitText)
ct.UpdateUI(func() error {
ct.Views.Menu.SetFrame(true)
ct.Views.Menu.Update(content)
ct.Views.Input.Write(value)
ct.Views.Input.SetCursor(len(value), 0)
return nil
})
return nil
}
// ShowPriceAlertsAddMenu shows the alert add menu
func (ct *Cointop) ShowPriceAlertsAddMenu() error {
ct.debuglog("showPriceAlertsAddMenu()")
ct.State.lastSelectedRowIndex = ct.HighlightedPageRowIndex()
ct.UpdatePriceAlertsUpdateMenu(true)
ct.ui.SetCursor(true)
ct.SetActiveView(ct.Views.Menu.Name())
ct.g.SetViewOnTop(ct.Views.Input.Name())
ct.g.SetCurrentView(ct.Views.Input.Name())
return nil
}
// ShowPriceAlertsUpdateMenu shows the alerts update menu
func (ct *Cointop) ShowPriceAlertsUpdateMenu() error {
ct.debuglog("showPriceAlertsUpdateMenu()")
ct.State.lastSelectedRowIndex = ct.HighlightedPageRowIndex()
ct.UpdatePriceAlertsUpdateMenu(false)
ct.ui.SetCursor(true)
ct.SetActiveView(ct.Views.Menu.Name())
ct.g.SetViewOnTop(ct.Views.Input.Name())
ct.g.SetCurrentView(ct.Views.Input.Name())
return nil
}
// HidePriceAlertsUpdateMenu hides the alerts update menu
func (ct *Cointop) HidePriceAlertsUpdateMenu() error {
ct.debuglog("hidePriceAlertsUpdateMenu()")
ct.ui.SetViewOnBottom(ct.Views.Menu)
ct.ui.SetViewOnBottom(ct.Views.Input)
ct.ui.SetCursor(false)
ct.SetActiveView(ct.Views.Table.Name())
ct.UpdateUI(func() error {
ct.Views.Menu.SetFrame(false)
ct.Views.Menu.Update("")
ct.Views.Input.Update("")
return nil
})
return nil
}
// EnterKeyPressHandler is the key press handle for update menus
func (ct *Cointop) EnterKeyPressHandler() error {
if ct.IsPortfolioVisible() {
return ct.SetPortfolioHoldings()
}
return ct.CreatePriceAlert()
}
// CreatePriceAlert sets price from inputed value
func (ct *Cointop) CreatePriceAlert() error {
ct.debuglog("createPriceAlert()")
defer ct.HidePriceAlertsUpdateMenu()
var coinName string
if ct.State.priceAlertEditID == "" {
coin := ct.HighlightedRowCoin()
coinName = coin.Name
} else {
for i, entry := range ct.State.priceAlerts.Entries {
if entry.ID == ct.State.priceAlertEditID {
coinName = ct.State.priceAlerts.Entries[i].CoinName
}
}
}
operator, targetPrice, err := ct.ReadAndParsePriceAlertInput()
if err != nil {
return err
}
if err := ct.SetPriceAlert(coinName, operator, targetPrice); err != nil {
return err
}
ct.UpdateTable()
ct.GoToPageRowIndex(ct.State.lastSelectedRowIndex)
return nil
}
// ReadAndParsePriceAlertInput reads and parses price alert input field value
func (ct *Cointop) ReadAndParsePriceAlertInput() (string, float64, error) {
// read input field
b := make([]byte, 100)
n, err := ct.Views.Input.Read(b)
if err != nil {
return "", 0, err
}
if n == 0 {
return "", 0, nil
}
inputValue := string(b)
operator, targetPrice, err := ct.ParsePriceAlertInput(inputValue)
if err != nil {
return "", 0, err
}
return operator, targetPrice, nil
}
// ParsePriceAlertInput parses price alert input field value
func (ct *Cointop) ParsePriceAlertInput(value string) (string, float64, error) {
regex := regexp.MustCompile(`(>|<|>=|<=|=)?\s*([0-9.]+).*`)
matches := regex.FindStringSubmatch(strings.TrimSpace(value))
operator := ""
amountValue := ""
if len(matches) == 2 {
amountValue = matches[1]
} else if len(matches) == 3 {
operator = matches[1]
amountValue = matches[2]
}
amountValue = normalizeFloatString(amountValue)
targetPrice, err := strconv.ParseFloat(amountValue, 64)
if err != nil {
return "", 0, err
}
return operator, targetPrice, nil
}
// SetPriceAlert sets a price alert
func (ct *Cointop) SetPriceAlert(coinName string, operator string, targetPrice float64) error {
ct.debuglog("setPriceAlert()")
if operator == "" {
operator = "="
}
if _, ok := PriceAlertOperatorMap[operator]; !ok {
return errors.New("price alert operator is invalid")
}
frequency := "once"
id := strings.ToLower(fmt.Sprintf("%s_%s_%v_%s", coinName, operator, targetPrice, frequency))
newEntry := &PriceAlert{
ID: id,
CoinName: coinName,
Operator: operator,
TargetPrice: targetPrice,
Frequency: frequency,
}
if ct.State.priceAlertEditID == "" {
ct.State.priceAlerts.Entries = append([]*PriceAlert{newEntry}, ct.State.priceAlerts.Entries...)
} else {
for i, entry := range ct.State.priceAlerts.Entries {
if entry.ID == ct.State.priceAlertEditID {
ct.State.priceAlerts.Entries[i] = newEntry
}
}
}
if err := ct.Save(); err != nil {
return err
}
return nil
}
// ActivePriceAlerts returns the active price alerts
func (ct *Cointop) ActivePriceAlerts() []*PriceAlert {
var filtered []*PriceAlert
for _, entry := range ct.State.priceAlerts.Entries {
if entry.Expired {
continue
}
filtered = append(filtered, entry)
}
return filtered
}
// ActivePriceAlertsLen returns the number of active price alerts
func (ct *Cointop) ActivePriceAlertsLen() int {
return len(ct.ActivePriceAlerts())
}

@ -30,6 +30,7 @@ func NewInputView() *InputView {
func (ct *Cointop) openSearch() error {
ct.debuglog("openSearch()")
ct.State.searchFieldVisible = true
ct.ui.SetCursor(true)
ct.SetActiveView(ct.Views.SearchField.Name())
return nil
}
@ -38,6 +39,7 @@ func (ct *Cointop) openSearch() error {
func (ct *Cointop) CancelSearch() error {
ct.debuglog("cancelSearch()")
ct.State.searchFieldVisible = false
ct.ui.SetCursor(false)
ct.SetActiveView(ct.Views.Table.Name())
return nil
}

@ -41,18 +41,24 @@ func (ct *Cointop) UpdateStatusbar(s string) error {
favoritesText = "[F]Favorites"
}
base := fmt.Sprintf("%s%s %sHelp %sChart %sRange %sSearch %sConvert %s %s %sSave", "[Q]", quitText, "[?]", "[Enter]", "[[ ]]", "[/]", "[C]", favoritesText, portfolioText, "[CTRL-S]")
str := pad.Right(fmt.Sprintf("%v %sPage %v/%v %s", base, "[← →]", currpage, totalpages, s), ct.maxTableWidth, " ")
v := fmt.Sprintf("%s", ct.Version())
end := len(str) - len(v) + 2
if end > len(str) {
end = len(str)
}
helpStr := fmt.Sprintf("%s%s %sHelp", "[Q]", quitText, "[?]")
var content string
if ct.IsPriceAlertsVisible() {
content = fmt.Sprintf("%s [E]Edit [+]Add", helpStr)
} else {
base := fmt.Sprintf("%s %sChart %sRange %sSearch %sConvert %s %s", helpStr, "[Enter]", "[[ ]]", "[/]", "[C]", favoritesText, portfolioText)
str := pad.Right(fmt.Sprintf("%v %sPage %v/%v %s", base, "[← →]", currpage, totalpages, s), ct.maxTableWidth, " ")
v := fmt.Sprintf("%s", ct.Version())
end := len(str) - len(v) + 2
if end > len(str) {
end = len(str)
}
str = str[:end] + v
content = str[:end] + v
}
ct.UpdateUI(func() error {
return ct.Views.Statusbar.Update(str)
return ct.Views.Statusbar.Update(content)
})
return nil

@ -47,8 +47,8 @@ func (ct *Cointop) RefreshTable() error {
switch ct.State.selectedView {
case PortfolioView:
ct.table = ct.GetPortfolioTable()
case AlertsView:
ct.table = ct.GetAlertsTable()
case PriceAlertsView:
ct.table = ct.GetPriceAlertsTable()
default:
ct.table = ct.GetCoinsTable()
}

@ -65,8 +65,8 @@ func (ct *Cointop) UpdateTableHeader() error {
switch ct.State.selectedView {
case PortfolioView:
cols = ct.GetPortfolioTableHeaders()
case AlertsView:
cols = ct.GetAlertsTableHeaders()
case PriceAlertsView:
cols = ct.GetPriceAlertsTableHeaders()
default:
cols = ct.GetCoinsTableHeaders()
}

@ -46,6 +46,11 @@ func (ui *UI) SetMouse(enabled bool) {
ui.g.Mouse = true
}
// SetCursor enables the input field cursor
func (ui *UI) SetCursor(enabled bool) {
ui.g.Cursor = enabled
}
// SetHighlight enables the highlight active state
func (ui *UI) SetHighlight(enabled bool) {
ui.g.Highlight = true

Loading…
Cancel
Save