From 184ebfb4974618fdfa120cac1486ed2a52e59888 Mon Sep 17 00:00:00 2001 From: Miguel Mota Date: Fri, 5 Feb 2021 01:04:09 -0800 Subject: [PATCH] Dynamic column widths --- cointop/coins_table.go | 258 ++++++++++++++++++++++------------------ cointop/cointop.go | 4 + cointop/colorscheme.go | 4 +- cointop/config.go | 18 +-- cointop/help.go | 2 +- cointop/keybindings.go | 68 ++--------- cointop/portfolio.go | 252 +++++++++++++++++++++------------------ cointop/price_alerts.go | 121 ++++++++++++------- cointop/sort.go | 2 +- cointop/table.go | 2 +- cointop/table_header.go | 255 +++++++++++++++++++++++++++++---------- cointop/util.go | 21 ++++ pkg/pad/pad.go | 6 +- pkg/table/table.go | 14 ++- 14 files changed, 616 insertions(+), 411 deletions(-) diff --git a/cointop/coins_table.go b/cointop/coins_table.go index a7e49a1..a668153 100644 --- a/cointop/coins_table.go +++ b/cointop/coins_table.go @@ -15,11 +15,11 @@ var DefaultCoinTableHeaders = []string{ "name", "symbol", "price", - "marketcap", - "24h_volume", "1h_change", "24h_change", "7d_change", + "24h_volume", + "market_cap", "total_supply", "available_supply", "last_updated", @@ -32,7 +32,6 @@ func (ct *Cointop) ValidCoinsTableHeader(name string) bool { return true } } - return false } @@ -45,168 +44,201 @@ func (ct *Cointop) GetCoinsTableHeaders() []string { func (ct *Cointop) GetCoinsTable() *table.Table { maxX := ct.width() t := table.NewTable().SetWidth(maxX) + var rows [][]*table.RowCell + headers := ct.GetCoinsTableHeaders() + ct.ClearSyncMap(ct.State.tableColumnWidths) + ct.ClearSyncMap(ct.State.tableColumnAlignLeft) for _, coin := range ct.State.coins { if coin == nil { continue } - star := ct.colorscheme.TableRow(" ") - if coin.Favorite { - star = ct.colorscheme.TableRowFavorite("*") - } - rank := fmt.Sprintf("%s%v", star, ct.colorscheme.TableRow(fmt.Sprintf("%6v ", coin.Rank))) - name := TruncateString(coin.Name, 20) - symbol := TruncateString(coin.Symbol, 6) - symbolpadding := 8 - // NOTE: this is to adjust padding by 1 because when all name rows are - // yellow it messes the spacing (need to debug) - if ct.IsFavoritesVisible() { - symbolpadding++ - } - namecolor := ct.colorscheme.TableRow - color1h := ct.colorscheme.TableColumnChange - color24h := ct.colorscheme.TableColumnChange - color7d := ct.colorscheme.TableColumnChange - if coin.Favorite { - namecolor = ct.colorscheme.TableRowFavorite - } - if coin.PercentChange1H > 0 { - color1h = ct.colorscheme.TableColumnChangeUp - } - if coin.PercentChange1H < 0 { - color1h = ct.colorscheme.TableColumnChangeDown - } - if coin.PercentChange24H > 0 { - color24h = ct.colorscheme.TableColumnChangeUp - } - if coin.PercentChange24H < 0 { - color24h = ct.colorscheme.TableColumnChangeDown - } - if coin.PercentChange7D > 0 { - color7d = ct.colorscheme.TableColumnChangeUp - } - if coin.PercentChange7D < 0 { - color7d = ct.colorscheme.TableColumnChangeDown - } - unix, _ := strconv.ParseInt(coin.LastUpdated, 10, 64) - lastUpdated := time.Unix(unix, 0).Format("15:04:05 Jan 02") - - headers := ct.GetCoinsTableHeaders() var rowCells []*table.RowCell for _, header := range headers { + leftMargin := 1 + rightMargin := 1 switch header { case "rank": + star := ct.colorscheme.TableRow(" ") + if coin.Favorite { + star = ct.colorscheme.TableRowFavorite("*") + } + rank := fmt.Sprintf("%s%v", star, ct.colorscheme.TableRow(fmt.Sprintf("%6v ", coin.Rank))) + ct.SetTableColumnWidth(header, 8) + ct.SetTableColumnAlignLeft(header, false) rowCells = append(rowCells, &table.RowCell{ - LeftMargin: 0, - Width: 6, - LeftAlign: false, - Color: ct.colorscheme.Default, - Text: rank, + LeftMargin: leftMargin, + RightMargin: rightMargin, + LeftAlign: false, + Color: ct.colorscheme.Default, + Text: rank, }) case "name": + name := TruncateString(coin.Name, 16) + namecolor := ct.colorscheme.TableRow + if coin.Favorite { + namecolor = ct.colorscheme.TableRowFavorite + } + ct.SetTableColumnWidthFromString(header, name) + ct.SetTableColumnAlignLeft(header, true) rowCells = append(rowCells, &table.RowCell{ - LeftMargin: 1, - Width: 22, - LeftAlign: true, - Color: namecolor, - Text: name, + LeftMargin: leftMargin, + RightMargin: rightMargin, + LeftAlign: true, + Color: namecolor, + Text: name, }) case "symbol": + symbol := TruncateString(coin.Symbol, 6) + ct.SetTableColumnWidthFromString(header, symbol) + ct.SetTableColumnAlignLeft(header, true) rowCells = append(rowCells, &table.RowCell{ - LeftMargin: 1, - Width: symbolpadding, - LeftAlign: true, - Color: ct.colorscheme.TableRow, - Text: symbol, + LeftMargin: leftMargin, + RightMargin: rightMargin, + LeftAlign: true, + Color: ct.colorscheme.TableRow, + Text: symbol, }) case "price": + text := humanize.Commaf(coin.Price) + ct.SetTableColumnWidthFromString(header, text) + ct.SetTableColumnAlignLeft(header, false) rowCells = append(rowCells, &table.RowCell{ - LeftMargin: 1, - Width: 12, - LeftAlign: false, - Color: ct.colorscheme.TableColumnPrice, - Text: humanize.Commaf(coin.Price), - }) - case "marketcap": - rowCells = append(rowCells, - &table.RowCell{ - LeftMargin: 1, - Width: 18, - LeftAlign: false, - Color: ct.colorscheme.TableRow, - Text: humanize.Commaf(coin.MarketCap), + LeftMargin: leftMargin, + RightMargin: rightMargin, + LeftAlign: false, + Color: ct.colorscheme.TableColumnPrice, + Text: text, }) case "24h_volume": + text := humanize.Commaf(coin.Volume24H) + ct.SetTableColumnWidthFromString(header, text) + ct.SetTableColumnAlignLeft(header, false) rowCells = append(rowCells, &table.RowCell{ - LeftMargin: 1, - Width: 16, - LeftAlign: false, - Color: ct.colorscheme.TableRow, - Text: humanize.Commaf(coin.Volume24H), + LeftMargin: leftMargin, + RightMargin: rightMargin, + LeftAlign: false, + Color: ct.colorscheme.TableRow, + Text: text, }) case "1h_change": + color1h := ct.colorscheme.TableColumnChange + if coin.PercentChange1H > 0 { + color1h = ct.colorscheme.TableColumnChangeUp + } + if coin.PercentChange1H < 0 { + color1h = ct.colorscheme.TableColumnChangeDown + } + text := fmt.Sprintf("%.2f%%", coin.PercentChange1H) + ct.SetTableColumnWidthFromString(header, text) + ct.SetTableColumnAlignLeft(header, false) rowCells = append(rowCells, &table.RowCell{ - LeftMargin: 1, - Width: 11, - LeftAlign: false, - Color: color1h, - Text: fmt.Sprintf("%.2f%%", coin.PercentChange1H), + LeftMargin: leftMargin, + RightMargin: rightMargin, + LeftAlign: false, + Color: color1h, + Text: text, }) case "24h_change": + color24h := ct.colorscheme.TableColumnChange + if coin.PercentChange24H > 0 { + color24h = ct.colorscheme.TableColumnChangeUp + } + if coin.PercentChange24H < 0 { + color24h = ct.colorscheme.TableColumnChangeDown + } + text := fmt.Sprintf("%.2f%%", coin.PercentChange24H) + ct.SetTableColumnWidthFromString(header, text) + ct.SetTableColumnAlignLeft(header, false) rowCells = append(rowCells, &table.RowCell{ - LeftMargin: 1, - Width: 10, - LeftAlign: false, - Color: color24h, - Text: fmt.Sprintf("%.2f%%", coin.PercentChange24H), + LeftMargin: leftMargin, + RightMargin: rightMargin, + LeftAlign: false, + Color: color24h, + Text: text, }) case "7d_change": + color7d := ct.colorscheme.TableColumnChange + if coin.PercentChange7D > 0 { + color7d = ct.colorscheme.TableColumnChangeUp + } + if coin.PercentChange7D < 0 { + color7d = ct.colorscheme.TableColumnChangeDown + } + text := fmt.Sprintf("%.2f%%", coin.PercentChange7D) + ct.SetTableColumnWidthFromString(header, text) + ct.SetTableColumnAlignLeft(header, false) rowCells = append(rowCells, &table.RowCell{ - LeftMargin: 1, - Width: 10, - LeftAlign: false, - Color: color7d, - Text: fmt.Sprintf("%.2f%%", coin.PercentChange7D), + LeftMargin: leftMargin, + RightMargin: rightMargin, + LeftAlign: false, + Color: color7d, + Text: text, + }) + case "market_cap": + text := humanize.Commaf(coin.MarketCap) + ct.SetTableColumnWidthFromString(header, text) + ct.SetTableColumnAlignLeft(header, false) + rowCells = append(rowCells, + &table.RowCell{ + LeftMargin: leftMargin, + RightMargin: rightMargin, + LeftAlign: false, + Color: ct.colorscheme.TableRow, + Text: text, }) case "total_supply": + text := humanize.Commaf(coin.TotalSupply) + ct.SetTableColumnWidthFromString(header, text) + ct.SetTableColumnAlignLeft(header, false) rowCells = append(rowCells, &table.RowCell{ - LeftMargin: 1, - Width: 22, - LeftAlign: false, - Color: ct.colorscheme.TableRow, - Text: humanize.Commaf(coin.TotalSupply), + LeftMargin: leftMargin, + RightMargin: rightMargin, + LeftAlign: false, + Color: ct.colorscheme.TableRow, + Text: text, }) case "available_supply": + text := humanize.Commaf(coin.AvailableSupply) + ct.SetTableColumnWidthFromString(header, text) + ct.SetTableColumnAlignLeft(header, false) rowCells = append(rowCells, &table.RowCell{ - LeftMargin: 1, - Width: 20, - LeftAlign: false, - Color: ct.colorscheme.TableRow, - Text: humanize.Commaf(coin.AvailableSupply), + LeftMargin: leftMargin, + RightMargin: rightMargin, + LeftAlign: false, + Color: ct.colorscheme.TableRow, + Text: text, }) case "last_updated": + unix, _ := strconv.ParseInt(coin.LastUpdated, 10, 64) + lastUpdated := time.Unix(unix, 0).Format("15:04:05 Jan 02") + ct.SetTableColumnWidthFromString(header, lastUpdated) + ct.SetTableColumnAlignLeft(header, false) rowCells = append(rowCells, &table.RowCell{ - LeftMargin: 1, - Width: 18, - LeftAlign: false, - Color: ct.colorscheme.TableRow, - Text: lastUpdated, + LeftMargin: leftMargin, + RightMargin: rightMargin, + LeftAlign: false, + Color: ct.colorscheme.TableRow, + Text: lastUpdated, }) } } + rows = append(rows, rowCells) + } - t.AddRowCells( - rowCells..., - // TODO: add %percent of cap - ) + for _, row := range rows { + for i, header := range headers { + row[i].Width = ct.GetTableColumnWidth(header) + } + t.AddRowCells(row...) } return t diff --git a/cointop/cointop.go b/cointop/cointop.go index 9b930d4..3c9afa9 100644 --- a/cointop/cointop.go +++ b/cointop/cointop.go @@ -75,6 +75,8 @@ type State struct { sortBy string tableOffsetX int onlyTable bool + tableColumnWidths sync.Map + tableColumnAlignLeft sync.Map chartHeight int priceAlerts *PriceAlerts priceAlertEditID string @@ -236,6 +238,8 @@ func NewCointop(config *Config) (*Cointop, error) { portfolioTableColumns: DefaultPortfolioTableHeaders, chartHeight: 10, tableOffsetX: 0, + tableColumnWidths: sync.Map{}, + tableColumnAlignLeft: sync.Map{}, priceAlerts: &PriceAlerts{ Entries: make([]*PriceAlert, 0), SoundEnabled: true, diff --git a/cointop/colorscheme.go b/cointop/colorscheme.go index c97f865..09945f1 100644 --- a/cointop/colorscheme.go +++ b/cointop/colorscheme.go @@ -239,6 +239,8 @@ func (c *Colorscheme) Default(a ...interface{}) string { } func (c *Colorscheme) toSprintf(name string) ISprintf { + c.cacheMutex.Lock() + defer c.cacheMutex.Unlock() if cached, ok := c.cache[name]; ok { return cached } @@ -265,9 +267,7 @@ func (c *Colorscheme) toSprintf(name string) ISprintf { } } - c.cacheMutex.Lock() c.cache[name] = fcolor.New(attrs...).SprintFunc() - c.cacheMutex.Unlock() return c.cache[name] } diff --git a/cointop/config.go b/cointop/config.go index 3e40f5b..161a6da 100644 --- a/cointop/config.go +++ b/cointop/config.go @@ -7,7 +7,6 @@ import ( "io/ioutil" "os" "path/filepath" - "reflect" "sort" "strconv" "strings" @@ -243,12 +242,8 @@ func (ct *Cointop) configToToml() ([]byte, error) { }) portfolioIfc["holdings"] = holdingsIfc - if reflect.DeepEqual(DefaultPortfolioTableHeaders, ct.State.portfolioTableColumns) { - portfolioIfc["columns"] = []string{} - } else { - var columnsIfc interface{} = ct.State.portfolioTableColumns - portfolioIfc["columns"] = columnsIfc - } + var columnsIfc interface{} = ct.State.portfolioTableColumns + portfolioIfc["columns"] = columnsIfc var currencyIfc interface{} = ct.State.currencyConversion var defaultViewIfc interface{} = ct.State.defaultView @@ -281,12 +276,7 @@ func (ct *Cointop) configToToml() ([]byte, error) { var coinsTableColumnsIfc interface{} = ct.State.coinsTableColumns tableMapIfc := map[string]interface{}{} - - if reflect.DeepEqual(DefaultCoinTableHeaders, ct.State.coinsTableColumns) { - tableMapIfc["columns"] = []string{} - } else { - tableMapIfc["columns"] = coinsTableColumnsIfc - } + tableMapIfc["columns"] = coinsTableColumnsIfc var inputs = &config{ API: apiChoiceIfc, @@ -374,6 +364,8 @@ func (ct *Cointop) loadDefaultViewFromConfig() error { ct.SetSelectedView(PortfolioView) case "favorites": ct.SetSelectedView(FavoritesView) + case "alerts", "price_alerts": + ct.SetSelectedView(PriceAlertsView) case "default": fallthrough default: diff --git a/cointop/help.go b/cointop/help.go index c8cf095..81f2ba8 100644 --- a/cointop/help.go +++ b/cointop/help.go @@ -44,7 +44,7 @@ func (ct *Cointop) UpdateHelp() { body = fmt.Sprintf("%s%s\n", body, row) } - versionLine := fmt.Sprintf("cointop %s - (C) 2017-2020 Miguel Mota", ct.Version()) + versionLine := fmt.Sprintf("cointop %s - (C) 2017-2021 Miguel Mota", ct.Version()) licenseLine := "Released under the Apache 2.0 License." instructionsLine := "List of keyboard shortcuts" infoLine := "See git.io/cointop for more info.\n Press ESC to return." diff --git a/cointop/keybindings.go b/cointop/keybindings.go index 2cc9b2e..0b8ada9 100644 --- a/cointop/keybindings.go +++ b/cointop/keybindings.go @@ -93,21 +93,9 @@ func (ct *Cointop) ParseKeys(s string) (interface{}, gocui.Modifier) { key = gocui.KeyCtrlZ case "~": key = gocui.KeyCtrlTilde - case "[": - fallthrough - case "lsqrbracket": - fallthrough - case "leftsqrbracket": - fallthrough - case "leftsquarebracket": + case "[", "lsqrbracket", "leftsqrbracket", "leftsquarebracket": key = gocui.KeyCtrlLsqBracket - case "]": - fallthrough - case "rsqrbracket": - fallthrough - case "rightsqrbracket": - fallthrough - case "rightsquarebracket": + case "]", "rsqrbracket", "rightsqrbracket", "rightsquarebracket": key = gocui.KeyCtrlRsqBracket case "space": key = gocui.KeyCtrlSpace @@ -130,41 +118,19 @@ func (ct *Cointop) ParseKeys(s string) (interface{}, gocui.Modifier) { s = strings.ToLower(s) switch s { - case "arrowup": - fallthrough - case "uparrow": - fallthrough - case "up": + case "arrowup", "uparrow", "up": key = gocui.KeyArrowUp - case "arrowdown": - fallthrough - case "downarrow": - fallthrough - case "down": + case "arrowdown", "downarrow", "down": key = gocui.KeyArrowDown - case "arrowleft": - fallthrough - case "leftarrow": - fallthrough - case "left": + case "arrowleft", "leftarrow", "left": key = gocui.KeyArrowLeft - case "arrowright": - fallthrough - case "rightarrow": - fallthrough - case "right": + case "arrowright", "rightarrow", "right": key = gocui.KeyArrowRight - case "enter": - fallthrough - case "return": + case "enter", "return": key = gocui.KeyEnter - case "space": - fallthrough - case "spacebar": + case "space", "spacebar": key = gocui.KeySpace - case "esc": - fallthrough - case "escape": + case "esc", "escape": key = gocui.KeyEsc case "f1": key = gocui.KeyF1 @@ -186,15 +152,9 @@ func (ct *Cointop) ParseKeys(s string) (interface{}, gocui.Modifier) { key = gocui.KeyF9 case "tab": key = gocui.KeyTab - case "pageup": - fallthrough - case "pgup": + case "pageup", "pgup": key = gocui.KeyPgup - case "pagedown": - fallthrough - case "pgdown": - fallthrough - case "pgdn": + case "pagedown", "pgdown", "pgdn": key = gocui.KeyPgdn case "home": key = gocui.KeyHome @@ -248,9 +208,7 @@ func (ct *Cointop) Keybindings(g *gocui.Gui) error { fn = ct.Keyfn(ct.SortPrevCol) case "sort_right_column": fn = ct.Keyfn(ct.SortNextCol) - case "help": - fallthrough - case "toggle_show_help": + case "help", "toggle_show_help": fn = ct.Keyfn(ct.ToggleHelp) view = "" case "show_help": @@ -276,7 +234,7 @@ func (ct *Cointop) Keybindings(g *gocui.Gui) error { case "move_to_page_visible_last_row": fn = ct.Keyfn(ct.navigatePageLastLine) case "sort_column_market_cap": - fn = ct.Sortfn("marketcap", true) + fn = ct.Sortfn("market_cap", true) case "move_to_page_visible_middle_row": fn = ct.Keyfn(ct.NavigatePageMiddleLine) case "scroll_left": diff --git a/cointop/portfolio.go b/cointop/portfolio.go index b774856..46bbf12 100644 --- a/cointop/portfolio.go +++ b/cointop/portfolio.go @@ -6,7 +6,6 @@ import ( "fmt" "math" "os" - "regexp" "sort" "strconv" "strings" @@ -54,157 +53,195 @@ func (ct *Cointop) GetPortfolioTable() *table.Table { total := ct.GetPortfolioTotal() maxX := ct.width() t := table.NewTable().SetWidth(maxX) - + var rows [][]*table.RowCell + headers := ct.GetPortfolioTableHeaders() + ct.ClearSyncMap(ct.State.tableColumnWidths) + ct.ClearSyncMap(ct.State.tableColumnAlignLeft) for _, coin := range ct.State.coins { - star := ct.colorscheme.TableRow(" ") - name := TruncateString(coin.Name, 20) - symbol := TruncateString(coin.Symbol, 6) - if coin.Favorite { - star = ct.colorscheme.TableRowFavorite("*") - } - rank := fmt.Sprintf("%s%v", star, ct.colorscheme.TableRow(fmt.Sprintf("%6v ", coin.Rank))) - - namecolor := ct.colorscheme.TableRow - if coin.Favorite { - namecolor = ct.colorscheme.TableRowFavorite - } - - colorbalance := ct.colorscheme.TableColumnPrice - color1h := ct.colorscheme.TableColumnChange - color24h := ct.colorscheme.TableColumnChange - color7d := ct.colorscheme.TableColumnChange - if coin.PercentChange1H > 0 { - color1h = ct.colorscheme.TableColumnChangeUp - } - if coin.PercentChange1H < 0 { - color1h = ct.colorscheme.TableColumnChangeDown - } - if coin.PercentChange24H > 0 { - color24h = ct.colorscheme.TableColumnChangeUp - } - if coin.PercentChange24H < 0 { - color24h = ct.colorscheme.TableColumnChangeDown - } - if coin.PercentChange7D > 0 { - color7d = ct.colorscheme.TableColumnChangeUp - } - if coin.PercentChange7D < 0 { - color7d = ct.colorscheme.TableColumnChangeDown - } - - percentHoldings := (coin.Balance / total) * 1e2 - if math.IsNaN(percentHoldings) { - percentHoldings = 0 - } - unix, _ := strconv.ParseInt(coin.LastUpdated, 10, 64) - lastUpdated := time.Unix(unix, 0).Format("15:04:05 Jan 02") - - headers := ct.GetPortfolioTableHeaders() + leftMargin := 1 + rightMargin := 1 var rowCells []*table.RowCell for _, header := range headers { switch header { case "rank": + star := ct.colorscheme.TableRow(" ") + if coin.Favorite { + star = ct.colorscheme.TableRowFavorite("*") + } + rank := fmt.Sprintf("%s%v", star, ct.colorscheme.TableRow(fmt.Sprintf("%6v ", coin.Rank))) + ct.SetTableColumnWidth(header, 8) + ct.SetTableColumnAlignLeft(header, false) rowCells = append(rowCells, &table.RowCell{ - LeftMargin: 0, - Width: 6, - LeftAlign: false, - Color: ct.colorscheme.Default, - Text: rank, + LeftMargin: leftMargin, + RightMargin: rightMargin, + LeftAlign: false, + Color: ct.colorscheme.Default, + Text: rank, }) case "name": + + name := TruncateString(coin.Name, 18) + namecolor := ct.colorscheme.TableRow + if coin.Favorite { + namecolor = ct.colorscheme.TableRowFavorite + } + ct.SetTableColumnWidthFromString(header, name) + ct.SetTableColumnAlignLeft(header, true) rowCells = append(rowCells, &table.RowCell{ - LeftMargin: 1, - Width: 22, - LeftAlign: true, - Color: namecolor, - Text: name, + LeftMargin: leftMargin, + RightMargin: rightMargin, + LeftAlign: true, + Color: namecolor, + Text: name, }) case "symbol": + symbol := TruncateString(coin.Symbol, 6) + ct.SetTableColumnWidthFromString(header, symbol) + ct.SetTableColumnAlignLeft(header, true) rowCells = append(rowCells, &table.RowCell{ - LeftMargin: 1, - Width: 6, - LeftAlign: true, - Color: ct.colorscheme.TableRow, - Text: symbol, + LeftMargin: leftMargin, + RightMargin: rightMargin, + LeftAlign: true, + Color: ct.colorscheme.TableRow, + Text: symbol, }) case "price": + text := humanize.Commaf(coin.Price) + symbolPadding := 1 + ct.SetTableColumnWidth(header, len(text)+symbolPadding) + ct.SetTableColumnAlignLeft(header, false) rowCells = append(rowCells, &table.RowCell{ - LeftMargin: 1, - Width: 14, - LeftAlign: false, - Color: ct.colorscheme.TableRow, - Text: humanize.Commaf(coin.Price), + LeftMargin: leftMargin, + RightMargin: rightMargin, + LeftAlign: false, + Color: ct.colorscheme.TableRow, + Text: text, }) case "holdings": + text := strconv.FormatFloat(coin.Holdings, 'f', -1, 64) + ct.SetTableColumnWidthFromString(header, text) + ct.SetTableColumnAlignLeft(header, false) rowCells = append(rowCells, &table.RowCell{ - LeftMargin: 1, - Width: 16, - LeftAlign: false, - Color: ct.colorscheme.TableRow, - Text: strconv.FormatFloat(coin.Holdings, 'f', -1, 64), + LeftMargin: leftMargin, + RightMargin: rightMargin, + LeftAlign: false, + Color: ct.colorscheme.TableRow, + Text: text, }) case "balance": + text := humanize.Commaf(coin.Balance) + ct.SetTableColumnWidthFromString(header, text) + ct.SetTableColumnAlignLeft(header, false) + colorBalance := ct.colorscheme.TableColumnPrice rowCells = append(rowCells, &table.RowCell{ - LeftMargin: 1, - Width: 16, - LeftAlign: false, - Color: colorbalance, - Text: humanize.Commaf(coin.Balance), + LeftMargin: leftMargin, + RightMargin: rightMargin, + LeftAlign: false, + Color: colorBalance, + Text: text, }) case "1h_change": + color1h := ct.colorscheme.TableColumnChange + if coin.PercentChange1H > 0 { + color1h = ct.colorscheme.TableColumnChangeUp + } + if coin.PercentChange1H < 0 { + color1h = ct.colorscheme.TableColumnChangeDown + } + text := fmt.Sprintf("%.2f%%", coin.PercentChange1H) + ct.SetTableColumnWidthFromString(header, text) + ct.SetTableColumnAlignLeft(header, false) rowCells = append(rowCells, &table.RowCell{ - LeftMargin: 1, - Width: 11, - LeftAlign: false, - Color: color1h, - Text: fmt.Sprintf("%.2f%%", coin.PercentChange1H), + LeftMargin: leftMargin, + RightMargin: rightMargin, + LeftAlign: false, + Color: color1h, + Text: text, }) case "24h_change": + color24h := ct.colorscheme.TableColumnChange + if coin.PercentChange24H > 0 { + color24h = ct.colorscheme.TableColumnChangeUp + } + if coin.PercentChange24H < 0 { + color24h = ct.colorscheme.TableColumnChangeDown + } + text := fmt.Sprintf("%.2f%%", coin.PercentChange24H) + ct.SetTableColumnWidthFromString(header, text) + ct.SetTableColumnAlignLeft(header, false) rowCells = append(rowCells, &table.RowCell{ - LeftMargin: 1, - Width: 10, - LeftAlign: false, - Color: color24h, - Text: fmt.Sprintf("%.2f%%", coin.PercentChange24H), + LeftMargin: leftMargin, + RightMargin: rightMargin, + LeftAlign: false, + Color: color24h, + Text: text, }) case "7d_change": + color7d := ct.colorscheme.TableColumnChange + if coin.PercentChange7D > 0 { + color7d = ct.colorscheme.TableColumnChangeUp + } + if coin.PercentChange7D < 0 { + color7d = ct.colorscheme.TableColumnChangeDown + } + text := fmt.Sprintf("%.2f%%", coin.PercentChange7D) + ct.SetTableColumnWidthFromString(header, text) + ct.SetTableColumnAlignLeft(header, false) rowCells = append(rowCells, &table.RowCell{ - LeftMargin: 1, - Width: 10, - LeftAlign: false, - Color: color7d, - Text: fmt.Sprintf("%.2f%%", coin.PercentChange7D), + LeftMargin: leftMargin, + RightMargin: rightMargin, + LeftAlign: false, + Color: color7d, + Text: text, }) case "percent_holdings": + percentHoldings := (coin.Balance / total) * 1e2 + if math.IsNaN(percentHoldings) { + percentHoldings = 0 + } + text := fmt.Sprintf("%.2f%%", percentHoldings) + ct.SetTableColumnWidthFromString(header, text) + ct.SetTableColumnAlignLeft(header, false) rowCells = append(rowCells, &table.RowCell{ - LeftMargin: 1, - Width: 14, - LeftAlign: false, - Color: ct.colorscheme.TableRow, - Text: fmt.Sprintf("%.2f%%", percentHoldings), + LeftMargin: leftMargin, + RightMargin: rightMargin, + LeftAlign: false, + Color: ct.colorscheme.TableRow, + Text: text, }) case "last_updated": + unix, _ := strconv.ParseInt(coin.LastUpdated, 10, 64) + lastUpdated := time.Unix(unix, 0).Format("15:04:05 Jan 02") + ct.SetTableColumnWidthFromString(header, lastUpdated) + ct.SetTableColumnAlignLeft(header, false) rowCells = append(rowCells, &table.RowCell{ - LeftMargin: 1, - Width: 18, - LeftAlign: false, - Color: ct.colorscheme.TableRow, - Text: lastUpdated, + LeftMargin: leftMargin, + RightMargin: rightMargin, + LeftAlign: false, + Color: ct.colorscheme.TableRow, + Text: lastUpdated, }) } } - t.AddRowCells(rowCells...) + rows = append(rows, rowCells) + } + + for _, row := range rows { + for i, header := range headers { + row[i].Width = ct.GetTableColumnWidth(header) + } + t.AddRowCells(row...) } return t @@ -752,14 +789,3 @@ func (ct *Cointop) PrintTotalHoldings(options *TablePrintOptions) error { func (ct *Cointop) IsPortfolioVisible() bool { return ct.State.selectedView == PortfolioView } - -// NormalizeFloatString normalizes a float as a string -func normalizeFloatString(input string) string { - re := regexp.MustCompile(`(\d+\.\d+|\.\d+|\d+)`) - result := re.FindStringSubmatch(input) - if len(result) > 0 { - return result[0] - } - - return "" -} diff --git a/cointop/price_alerts.go b/cointop/price_alerts.go index cc9956f..8b2708f 100644 --- a/cointop/price_alerts.go +++ b/cointop/price_alerts.go @@ -45,7 +45,10 @@ func (ct *Cointop) GetPriceAlertsTable() *table.Table { ct.debuglog("getPriceAlertsTable()") maxX := ct.width() t := table.NewTable().SetWidth(maxX) - + var rows [][]*table.RowCell + headers := ct.GetPriceAlertsTableHeaders() + ct.ClearSyncMap(ct.State.tableColumnWidths) + ct.ClearSyncMap(ct.State.tableColumnAlignLeft) for _, entry := range ct.State.priceAlerts.Entries { if entry.Expired { continue @@ -58,53 +61,83 @@ func (ct *Cointop) GetPriceAlertsTable() *table.Table { if !ok { continue } - name := TruncateString(entry.CoinName, 20) - symbol := TruncateString(coin.Symbol, 6) - namecolor := ct.colorscheme.TableRow - frequency := entry.Frequency _, ok = PriceAlertOperatorMap[entry.Operator] if !ok { continue } - targetPrice := fmt.Sprintf("%s %s", entry.Operator, humanize.Commaf(entry.TargetPrice)) - - t.AddRowCells( - &table.RowCell{ - LeftMargin: 1, - Width: 22, - LeftAlign: true, - Color: namecolor, - Text: name, - }, - &table.RowCell{ - LeftMargin: 1, - Width: 10, - LeftAlign: true, - Color: ct.colorscheme.TableRow, - Text: symbol, - }, - &table.RowCell{ - LeftMargin: 1, - Width: 14, - LeftAlign: false, - Color: ct.colorscheme.TableColumnPrice, - Text: targetPrice, - }, - &table.RowCell{ - LeftMargin: 1, - Width: 11, - LeftAlign: false, - Color: ct.colorscheme.TableRow, - Text: humanize.Commaf(coin.Price), - }, - &table.RowCell{ - LeftMargin: 4, - Width: 10, - LeftAlign: true, - Color: ct.colorscheme.TableRow, - Text: frequency, - }, - ) + + leftMargin := 1 + rightMargin := 1 + var rowCells []*table.RowCell + for _, header := range headers { + switch header { + case "name": + name := TruncateString(entry.CoinName, 16) + ct.SetTableColumnWidthFromString(header, name) + ct.SetTableColumnAlignLeft(header, true) + namecolor := ct.colorscheme.TableRow + rowCells = append(rowCells, &table.RowCell{ + LeftMargin: leftMargin, + RightMargin: rightMargin, + LeftAlign: true, + Color: namecolor, + Text: name, + }) + case "symbol": + symbol := TruncateString(coin.Symbol, 6) + ct.SetTableColumnWidthFromString(header, symbol) + ct.SetTableColumnAlignLeft(header, true) + rowCells = append(rowCells, &table.RowCell{ + LeftMargin: leftMargin, + RightMargin: rightMargin, + LeftAlign: true, + Color: ct.colorscheme.TableRow, + Text: symbol, + }) + + case "target_price": + targetPrice := fmt.Sprintf("%s %s", entry.Operator, humanize.Commaf(entry.TargetPrice)) + ct.SetTableColumnWidthFromString(header, targetPrice) + ct.SetTableColumnAlignLeft(header, false) + rowCells = append(rowCells, &table.RowCell{ + LeftMargin: leftMargin, + RightMargin: rightMargin, + LeftAlign: false, + Color: ct.colorscheme.TableColumnPrice, + Text: targetPrice, + }) + case "price": + text := humanize.Commaf(coin.Price) + ct.SetTableColumnWidthFromString(header, text) + ct.SetTableColumnAlignLeft(header, false) + rowCells = append(rowCells, &table.RowCell{ + LeftMargin: leftMargin, + RightMargin: rightMargin, + LeftAlign: false, + Color: ct.colorscheme.TableRow, + Text: text, + }) + case "frequency": + frequency := entry.Frequency + ct.SetTableColumnWidthFromString(header, frequency) + ct.SetTableColumnAlignLeft(header, true) + rowCells = append(rowCells, &table.RowCell{ + LeftMargin: leftMargin, + RightMargin: rightMargin, + LeftAlign: true, + Color: ct.colorscheme.TableRow, + Text: frequency, + }) + } + } + rows = append(rows, rowCells) + } + + for _, row := range rows { + for i, header := range headers { + row[i].Width = ct.GetTableColumnWidth(header) + } + t.AddRowCells(row...) } return t diff --git a/cointop/sort.go b/cointop/sort.go index ca2785e..21f27be 100644 --- a/cointop/sort.go +++ b/cointop/sort.go @@ -47,7 +47,7 @@ func (ct *Cointop) Sort(sortBy string, desc bool, list []*Coin, renderHeaders bo return a.Holdings < b.Holdings case "balance": return a.Balance < b.Balance - case "marketcap": + case "market_cap": return a.MarketCap < b.MarketCap case "24h_volume": return a.Volume24H < b.Volume24H diff --git a/cointop/table.go b/cointop/table.go index c76c18f..4912124 100644 --- a/cointop/table.go +++ b/cointop/table.go @@ -26,7 +26,7 @@ func TableColumnOrder() []string { "price", "holdings", "balance", - "marketcap", + "market_cap", "24h_volume", "1h_change", "7d_change", diff --git a/cointop/table_header.go b/cointop/table_header.go index d748224..bc16d1d 100644 --- a/cointop/table_header.go +++ b/cointop/table_header.go @@ -2,11 +2,116 @@ package cointop import ( "fmt" + "math" "strings" + "unicode/utf8" + "github.com/miguelmota/cointop/pkg/pad" "github.com/miguelmota/cointop/pkg/ui" ) +// ArrowUp is up arrow unicode character +var ArrowUp = "▲" + +// ArrowDown is down arrow unicode character +var ArrowDown = "▼" + +// HeaderColumn is header column struct +type HeaderColumn struct { + Slug string + Label string + PlainLabel string +} + +// HeaderColumns are the header column widths +var HeaderColumns = map[string]*HeaderColumn{ + "rank": &HeaderColumn{ + Slug: "rank", + Label: "[r]ank", + PlainLabel: "rank", + }, + "name": &HeaderColumn{ + Slug: "name", + Label: "[n]ame", + PlainLabel: "name", + }, + "symbol": &HeaderColumn{ + Slug: "symbol", + Label: "[s]ymbol", + PlainLabel: "symbol", + }, + "target_price": &HeaderColumn{ + Slug: "target_price", + Label: "[t]target price", + PlainLabel: "target price", + }, + "price": &HeaderColumn{ + Slug: "price", + Label: "[p]rice", + PlainLabel: "price", + }, + "frequency": &HeaderColumn{ + Slug: "frequency", + Label: "frequency", + PlainLabel: "frequency", + }, + "holdings": &HeaderColumn{ + Slug: "holdings", + Label: "[h]oldings", + PlainLabel: "holdings", + }, + "balance": &HeaderColumn{ + Slug: "balance", + Label: "[b]alance", + PlainLabel: "balance", + }, + "market_cap": &HeaderColumn{ + Slug: "market_cap", + Label: "[m]arket cap", + PlainLabel: "market cap", + }, + "24h_volume": &HeaderColumn{ + Slug: "24h_volume", + Label: "24H [v]olume", + PlainLabel: "24H volume", + }, + "1h_change": &HeaderColumn{ + Slug: "1h_change", + Label: "[1]H%", + PlainLabel: "1H%", + }, + "24h_change": &HeaderColumn{ + Slug: "24h_change", + Label: "[2]4H%", + PlainLabel: "24H%", + }, + "7d_change": &HeaderColumn{ + Slug: "7d_change", + Label: "[7]D%", + PlainLabel: "7D%", + }, + "total_supply": &HeaderColumn{ + Slug: "total_supply", + Label: "[t]otal supply", + PlainLabel: "total supply", + }, + "available_supply": &HeaderColumn{ + Slug: "available_supply", + Label: "[a]vailable supply", + PlainLabel: "available supply", + }, + "percent_holdings": &HeaderColumn{ + Slug: "percent_holdings", + Label: "[%]holdings", + PlainLabel: "%holdings", + }, + "last_updated": &HeaderColumn{ + Slug: "last_updated", + Label: "last [u]pdated", + PlainLabel: "last updated", + }, +} + // TableHeaderView is structure for table header view type TableHeaderView = ui.View @@ -20,56 +125,8 @@ func NewTableHeaderView() *TableHeaderView { func (ct *Cointop) UpdateTableHeader() error { ct.debuglog("UpdateTableHeader()") - type t struct { - colorfn func(a ...interface{}) string - displaytext string - padleft int - padright int - arrow string - } - baseColor := ct.colorscheme.TableHeaderSprintf() - offset := 0 - lb := "[" - rb := "]" noSort := ct.IsPriceAlertsVisible() - if noSort { - offset = 2 - lb = "" - rb = "" - } - possibleHeaders := map[string]*t{ - "rank": {baseColor, fmt.Sprintf("%sr%sank", lb, rb), 0, 1 + offset, " "}, - "name": {baseColor, fmt.Sprintf("%sn%same", lb, rb), 0, 11 + offset, " "}, - "symbol": {baseColor, fmt.Sprintf("%ss%symbol", lb, rb), 4, 0 + offset, " "}, - "target_price": {baseColor, fmt.Sprintf("%st%sarget price", lb, rb), 2, 0 + offset, " "}, - "price": {baseColor, fmt.Sprintf("%sp%srice", lb, rb), 2, 0 + offset, " "}, - "frequency": {baseColor, "frequency", 1, 0, " "}, - "holdings": {baseColor, fmt.Sprintf("%sh%soldings", lb, rb), 5, 0 + offset, " "}, - "balance": {baseColor, fmt.Sprintf("%sb%salance", lb, rb), 5, 0, " "}, - "marketcap": {baseColor, fmt.Sprintf("%sm%sarket cap", lb, rb), 5, 0 + offset, " "}, - "24h_volume": {baseColor, fmt.Sprintf("24H %sv%solume", lb, rb), 3, 0 + offset, " "}, - "1h_change": {baseColor, fmt.Sprintf("%s1%sH%%", lb, rb), 5, 0 + offset, " "}, - "24h_change": {baseColor, fmt.Sprintf("%s2%s4H%%", lb, rb), 3, 0 + offset, " "}, - "7d_change": {baseColor, fmt.Sprintf("%s7%sD%%", lb, rb), 4, 0 + offset, " "}, - "total_supply": {baseColor, fmt.Sprintf("%st%sotal supply", lb, rb), 7, 0 + offset, " "}, - "available_supply": {baseColor, fmt.Sprintf("%sa%svailable supply", lb, rb), 1, 0 + offset, " "}, - "percent_holdings": {baseColor, fmt.Sprintf("%s%%%sholdings", lb, rb), 2, 0 + offset, " "}, - "last_updated": {baseColor, fmt.Sprintf("last %su%spdated", lb, rb), 3, 0, " "}, - } - - for k := range possibleHeaders { - possibleHeaders[k].arrow = " " - if ct.State.sortBy == k { - possibleHeaders[k].colorfn = ct.colorscheme.TableHeaderColumnActiveSprintf() - if ct.State.sortDesc { - possibleHeaders[k].arrow = "▼" - } else { - possibleHeaders[k].arrow = "▲" - } - } - } - var cols []string switch ct.State.selectedView { case PortfolioView: @@ -81,24 +138,56 @@ func (ct *Cointop) UpdateTableHeader() error { } var headers []string - for _, v := range cols { - s, ok := possibleHeaders[v] + for i, col := range cols { + hc, ok := HeaderColumns[col] if !ok { continue } - var str string - d := s.arrow + s.displaytext - if v == "price" || v == "balance" { - d = s.arrow + ct.CurrencySymbol() + s.displaytext + width := ct.GetTableColumnWidth(col) + if width == 0 { + continue } - - str = fmt.Sprintf( + arrow := " " + colorfn := baseColor + if !noSort { + if ct.State.sortBy == col { + colorfn = ct.colorscheme.TableHeaderColumnActiveSprintf() + if ct.State.sortDesc { + arrow = ArrowDown + } else { + arrow = ArrowUp + } + } + } + label := hc.Label + if noSort { + label = hc.PlainLabel + } + leftAlign := ct.GetTableColumnAlignLeft(col) + switch col { + case "price", "balance": + label = ct.CurrencySymbol() + label + } + if leftAlign { + label = label + arrow + } else { + label = arrow + label + } + padfn := pad.Left + padLeft := 1 + if !noSort && i == 0 { + padLeft = 0 + } + if leftAlign { + padfn = pad.Right + } + colStr := fmt.Sprintf( "%s%s%s", - strings.Repeat(" ", s.padleft), - s.colorfn(d), - strings.Repeat(" ", s.padright), + strings.Repeat(" ", padLeft), + colorfn(padfn(label, width+(1-padLeft), " ")), + strings.Repeat(" ", 1), ) - headers = append(headers, str) + headers = append(headers, colStr) } ct.UpdateUI(func() error { @@ -107,3 +196,49 @@ func (ct *Cointop) UpdateTableHeader() error { return nil } + +// SetTableColumnAlignLeft sets the column alignment direction for header +func (ct *Cointop) SetTableColumnAlignLeft(header string, alignLeft bool) { + ct.State.tableColumnAlignLeft.Store(header, alignLeft) +} + +// GetTableColumnAlignLeft gets the column alignment direction for header +func (ct *Cointop) GetTableColumnAlignLeft(header string) bool { + ifc, ok := ct.State.tableColumnAlignLeft.Load(header) + if ok { + return ifc.(bool) + } + return false +} + +// SetTableColumnWidth sets the column width for header +func (ct *Cointop) SetTableColumnWidth(header string, width int) { + prevIfc, ok := ct.State.tableColumnWidths.Load(header) + var prev int + if ok { + prev = prevIfc.(int) + } else { + hc := HeaderColumns[header] + prev = utf8.RuneCountInString(hc.Label) + 1 + switch header { + case "price", "balance": + prev++ + } + } + + ct.State.tableColumnWidths.Store(header, int(math.Max(float64(width), float64(prev)))) +} + +// SetTableColumnWidthFromString sets the column width for header given size of string +func (ct *Cointop) SetTableColumnWidthFromString(header string, text string) { + ct.SetTableColumnWidth(header, utf8.RuneCountInString(text)) +} + +// GetTableColumnWidth gets the column width for header +func (ct *Cointop) GetTableColumnWidth(header string) int { + ifc, ok := ct.State.tableColumnWidths.Load(header) + if ok { + return ifc.(int) + } + return 0 +} diff --git a/cointop/util.go b/cointop/util.go index b243db5..e2b7d2f 100644 --- a/cointop/util.go +++ b/cointop/util.go @@ -4,7 +4,9 @@ import ( "bytes" "encoding/gob" "fmt" + "regexp" "strings" + "sync" "github.com/miguelmota/cointop/pkg/open" ) @@ -41,3 +43,22 @@ func TruncateString(value string, maxLen int) string { } return value } + +// ClearSyncMap clears a sync.Map +func (ct *Cointop) ClearSyncMap(syncMap sync.Map) { + syncMap.Range(func(key interface{}, value interface{}) bool { + syncMap.Delete(key) + return true + }) +} + +// NormalizeFloatString normalizes a float as a string +func normalizeFloatString(input string) string { + re := regexp.MustCompile(`(\d+\.\d+|\.\d+|\d+)`) + result := re.FindStringSubmatch(input) + if len(result) > 0 { + return result[0] + } + + return "" +} diff --git a/pkg/pad/pad.go b/pkg/pad/pad.go index 69a89ab..8c95290 100644 --- a/pkg/pad/pad.go +++ b/pkg/pad/pad.go @@ -1,5 +1,7 @@ package pad +import "unicode/utf8" + func times(str string, n int) (out string) { for i := 0; i < n; i++ { out += str @@ -10,10 +12,10 @@ func times(str string, n int) (out string) { // Left left-pads the string with pad up to len runes // len may be exceeded if func Left(str string, length int, pad string) string { - return times(pad, length-len(str)) + str + return times(pad, length-utf8.RuneCountInString(str)) + str } // Right right-pads the string with pad up to len runes func Right(str string, length int, pad string) string { - return str + times(pad, length-len(str)) + return str + times(pad, length-utf8.RuneCountInString(str)) } diff --git a/pkg/table/table.go b/pkg/table/table.go index 69f5156..b65f447 100644 --- a/pkg/table/table.go +++ b/pkg/table/table.go @@ -248,20 +248,22 @@ func (t *Table) Fprint(w io.Writer) { // RowCell is a row cell struct type RowCell struct { - LeftMargin int - Width int - LeftAlign bool - Color func(a ...interface{}) string - Text string + LeftMargin int + RightMargin int + Width int + LeftAlign bool + Color func(a ...interface{}) string + Text string } // String returns row cell as string func (rc *RowCell) String() string { - t := strings.Repeat(" ", rc.LeftMargin) + rc.Text + t := rc.Text if rc.LeftAlign { t = pad.Right(t, rc.Width, " ") } else { t = fmt.Sprintf("%"+fmt.Sprintf("%v", rc.Width)+"s", t) } + t = strings.Repeat(" ", rc.LeftMargin) + t + strings.Repeat(" ", rc.RightMargin) return rc.Color(t) }