diff --git a/cointop/active_view.go b/cointop/active_view.go index ac99a0a..778e19b 100644 --- a/cointop/active_view.go +++ b/cointop/active_view.go @@ -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 } diff --git a/cointop/cointop.go b/cointop/cointop.go index b01c896..9037462 100644 --- a/cointop/cointop.go +++ b/cointop/cointop.go @@ -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(), }, } diff --git a/cointop/config.go b/cointop/config.go index f3736f3..54fbdb9 100644 --- a/cointop/config.go +++ b/cointop/config.go @@ -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, } diff --git a/cointop/constants.go b/cointop/constants.go index c7fb6f3..2c1d5cb 100644 --- a/cointop/constants.go +++ b/cointop/constants.go @@ -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" diff --git a/cointop/conversion.go b/cointop/conversion.go index 404464e..74865c2 100644 --- a/cointop/conversion.go +++ b/cointop/conversion.go @@ -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 } diff --git a/cointop/default_shortcuts.go b/cointop/default_shortcuts.go index 75c5341..2d819aa 100644 --- a/cointop/default_shortcuts.go +++ b/cointop/default_shortcuts.go @@ -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", } } diff --git a/cointop/help.go b/cointop/help.go index cc4dd44..98e4782 100644 --- a/cointop/help.go +++ b/cointop/help.go @@ -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 } diff --git a/cointop/keybindings.go b/cointop/keybindings.go index 80765d0..12d69a8 100644 --- a/cointop/keybindings.go +++ b/cointop/keybindings.go @@ -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 diff --git a/cointop/layout.go b/cointop/layout.go index 635abe2..62d21bf 100644 --- a/cointop/layout.go +++ b/cointop/layout.go @@ -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() } diff --git a/cointop/menu.go b/cointop/menu.go new file mode 100644 index 0000000..04ec7b6 --- /dev/null +++ b/cointop/menu.go @@ -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 +} diff --git a/cointop/navigation.go b/cointop/navigation.go index f2528e6..cbd9622 100644 --- a/cointop/navigation.go +++ b/cointop/navigation.go @@ -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) +} diff --git a/cointop/portfolio.go b/cointop/portfolio.go index a6c14c7..fb79f92 100644 --- a/cointop/portfolio.go +++ b/cointop/portfolio.go @@ -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 } diff --git a/cointop/price_alerts.go b/cointop/price_alerts.go index cde98d7..6bc4693 100644 --- a/cointop/price_alerts.go +++ b/cointop/price_alerts.go @@ -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()) +} diff --git a/cointop/search.go b/cointop/search.go index 696547f..9e20935 100644 --- a/cointop/search.go +++ b/cointop/search.go @@ -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 } diff --git a/cointop/statusbar.go b/cointop/statusbar.go index d6ec892..8269f44 100644 --- a/cointop/statusbar.go +++ b/cointop/statusbar.go @@ -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 diff --git a/cointop/table.go b/cointop/table.go index 3de4fe5..6cad71d 100644 --- a/cointop/table.go +++ b/cointop/table.go @@ -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() } diff --git a/cointop/table_header.go b/cointop/table_header.go index 758b705..4781ce2 100644 --- a/cointop/table_header.go +++ b/cointop/table_header.go @@ -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() } diff --git a/pkg/ui/ui.go b/pkg/ui/ui.go index 868ae1b..3366bef 100644 --- a/pkg/ui/ui.go +++ b/pkg/ui/ui.go @@ -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