commit cb8ecd1b3d8b754688bb4ea7235a96b562fd4808 Author: Miguel Mota Date: Thu Mar 29 15:02:24 2018 -0700 init diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e69de29 diff --git a/README.md b/README.md new file mode 100644 index 0000000..9dd4732 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# cointop + +> Coin tracking for hackers + +## License + +MIT diff --git a/cointop/cointop.go b/cointop/cointop.go new file mode 100644 index 0000000..4860595 --- /dev/null +++ b/cointop/cointop.go @@ -0,0 +1,614 @@ +package main + +import ( + "fmt" + "log" + "math" + "strconv" + "time" + + "github.com/bradfitz/slice" + humanize "github.com/dustin/go-humanize" + "github.com/fatih/color" + "github.com/gizak/termui" + "github.com/jroimartin/gocui" + "github.com/miguelmota/cointop/table" + cmc "github.com/miguelmota/go-coinmarketcap" + "github.com/willf/pad" +) + +var white = color.New(color.FgWhite).SprintFunc() +var cyan = color.New(color.FgCyan).SprintFunc() +var red = color.New(color.FgRed).SprintFunc() +var green = color.New(color.FgGreen).SprintFunc() + +func fmtTime(v interface{}) string { + t := v.(time.Time) + return t.Format("2006-01-02 15:04:05") +} + +func ltTime(a interface{}, b interface{}) bool { + return a.(time.Time).Before(b.(time.Time)) +} + +func round(num float64) int { + return int(num + math.Copysign(0.5, num)) +} + +func toFixed(num float64, precision int) float64 { + output := math.Pow(10, float64(precision)) + return float64(round(num*output)) / output +} + +func chartCells(maxX int, coin string) [][]termui.Cell { + //_ = termui.Init() + //defer termui.Close() + + // quit on Ctrl-c + /* + termui.Handle("/sys/kbd/C-c", func(termui.Event) { + termui.StopLoop() + }) + */ + chart := termui.NewLineChart() + //chart.DataLabels = []string{""} + chart.Height = 10 + chart.AxesColor = termui.ColorWhite + chart.LineColor = termui.ColorCyan //| termui.AttrBold + chart.Border = false + + var ( + oneMinute int64 = 60 + oneHour = oneMinute * 60 + oneDay = oneHour * 24 + //oneWeek = oneDay * 7 + //oneMonth = oneDay * 30 + //oneYear = oneDay * 365 + ) + + now := time.Now() + secs := now.Unix() + start := secs - oneDay + end := secs + + _ = coin + //graphData, err := cmc.GetCoinGraphData(coin, start, end) + graphData, err := cmc.GetGlobalMarketGraphData(start, end) + if err != nil { + log.Fatal(err) + } + + var data []float64 + /* + for i := range graphData.PriceUSD { + data = append(data, graphData.PriceUSD[i][1]) + } + */ + for i := range graphData.MarketCapByAvailableSupply { + data = append(data, graphData.MarketCapByAvailableSupply[i][1]) + } + chart.Data = data + //h := chart.InnerHeight() + //w := chart.InnerWidth() + + // add grid rows and columns + termui.Body = termui.NewGrid() + //termui.Body.Rows = termui.Body.Rows[:0] + termui.Body.Width = maxX + termui.Body.AddRows( + termui.NewRow( + termui.NewCol(12, 0, chart), + ), + ) + + var points [][]termui.Cell + + // calculate layout + termui.Body.Align() + // render to terminal + //termui.Render(termui.Body) + w := termui.Body.Width + h := 10 + row := termui.Body.Rows[0] + b := row.Buffer() + for i := 0; i < h; i = i + 1 { + var rowpoints []termui.Cell + for j := 0; j < w; j = j + 1 { + _ = b + p := b.At(j, i) + c := p + rowpoints = append(rowpoints, c) + } + points = append(points, rowpoints) + } + //termui.Loop() + + return points +} + +var points [][]termui.Cell + +func main() { + g, err := gocui.NewGui(gocui.Output256) + if err != nil { + log.Fatalf("new gocui: %v", err) + } + defer g.Close() + + g.Cursor = true + g.Mouse = true + g.Highlight = true + g.SetManagerFunc(layout) + + if err := keybindings(g); err != nil { + log.Fatalf("keybindings: %v", err) + } + + if err := g.MainLoop(); err != nil && err != gocui.ErrQuit { + log.Fatalf("main loop: %v", err) + } +} + +func quit(g *gocui.Gui, v *gocui.View) error { + return gocui.ErrQuit +} + +func keybindings(g *gocui.Gui) error { + if err := g.SetKeybinding("", gocui.KeyArrowDown, gocui.ModNone, cursorDown); err != nil { + return err + } + + if err := g.SetKeybinding("", 'j', gocui.ModNone, cursorDown); err != nil { + return err + } + + if err := g.SetKeybinding("", gocui.KeyArrowUp, gocui.ModNone, cursorUp); err != nil { + return err + } + + if err := g.SetKeybinding("", 'k', gocui.ModNone, cursorUp); err != nil { + return err + } + + if err := g.SetKeybinding("", gocui.KeyCtrlD, gocui.ModNone, pageDown); err != nil { + return err + } + + if err := g.SetKeybinding("", gocui.KeyCtrlU, gocui.ModNone, pageUp); err != nil { + return err + } + + if err := g.SetKeybinding("", 'r', gocui.ModNone, sort("rank", false)); err != nil { + return err + } + if err := g.SetKeybinding("", 'n', gocui.ModNone, sort("name", true)); err != nil { + return err + } + if err := g.SetKeybinding("", 's', gocui.ModNone, sort("symbol", false)); err != nil { + return err + } + if err := g.SetKeybinding("", 'p', gocui.ModNone, sort("price", true)); err != nil { + return err + } + if err := g.SetKeybinding("", 'm', gocui.ModNone, sort("marketcap", true)); err != nil { + return err + } + if err := g.SetKeybinding("", 'v', gocui.ModNone, sort("24hvolume", true)); err != nil { + return err + } + if err := g.SetKeybinding("", '1', gocui.ModNone, sort("1hchange", true)); err != nil { + return err + } + if err := g.SetKeybinding("", '2', gocui.ModNone, sort("24hchange", true)); err != nil { + return err + } + if err := g.SetKeybinding("", '7', gocui.ModNone, sort("7dchange", true)); err != nil { + return err + } + if err := g.SetKeybinding("", 't', gocui.ModNone, sort("totalsupply", true)); err != nil { + return err + } + if err := g.SetKeybinding("", 'a', gocui.ModNone, sort("availablesupply", true)); err != nil { + return err + } + if err := g.SetKeybinding("", 'l', gocui.ModNone, sort("lastupdated", true)); err != nil { + return err + } + + if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil { + return err + } + if err := g.SetKeybinding("", 'q', gocui.ModNone, quit); err != nil { + return err + } + if err := g.SetKeybinding("", gocui.KeyEsc, gocui.ModNone, quit); err != nil { + return err + } + + return nil +} + +type strategy func(g *gocui.Gui, v *gocui.View) error + +var sortDesc bool +var currentsort string + +func sort(sortBy string, desc bool) strategy { + return func(g *gocui.Gui, v *gocui.View) error { + + if currentsort == sortBy { + sortDesc = !sortDesc + } else { + currentsort = sortBy + sortDesc = desc + } + + slice.Sort(coins[:], func(i, j int) bool { + if sortDesc == true { + i, j = j, i + } + a := coins[i] + b := coins[j] + switch sortBy { + case "rank": + return a.Rank < b.Rank + case "name": + return a.Name < b.Name + case "symbol": + return a.Symbol < b.Symbol + case "price": + return a.PriceUSD < b.PriceUSD + case "marketcap": + return a.MarketCapUSD < b.MarketCapUSD + case "24hvolume": + return a.USD24HVolume < b.USD24HVolume + case "1hchange": + return a.PercentChange1H < b.PercentChange1H + case "24hchange": + return a.PercentChange24H < b.PercentChange24H + case "7dchange": + return a.PercentChange7D < b.PercentChange7D + case "totalsupply": + return a.TotalSupply < b.TotalSupply + case "availablesupply": + return a.AvailableSupply < b.AvailableSupply + case "lastupdated": + return a.LastUpdated < b.LastUpdated + default: + return a.Rank < b.Rank + } + }) + + g.Update(func(g *gocui.Gui) error { + v, _ = g.View("table") + v.Clear() + setTable(g) + return nil + }) + + g.Update(func(g *gocui.Gui) error { + v, _ = g.View("chart") + v.Clear() + + maxX, _ := g.Size() + _, cy := v.Cursor() + coin := "ethereum" + points = chartCells(maxX, coin) + setChart(g, v) + fmt.Fprint(v, cy) + return nil + }) + + return nil + } +} + +var coins []*cmc.Coin + +func cursorDown(g *gocui.Gui, v *gocui.View) error { + v, _ = g.View("table") + if v != nil { + _, y := v.Origin() + cx, cy := v.Cursor() + numRows := len(coins) - 1 + //fmt.Fprint(v, cy) + if (cy + y + 1) > numRows { + return nil + } + if err := v.SetCursor(cx, cy+1); err != nil { + ox, oy := v.Origin() + if err := v.SetOrigin(ox, oy+1); err != nil { + return err + } + } + } + return nil +} + +func cursorUp(g *gocui.Gui, v *gocui.View) error { + v, _ = g.View("table") + if v != nil { + ox, oy := v.Origin() + cx, cy := v.Cursor() + //fmt.Fprint(v, oy) + if err := v.SetCursor(cx, cy-1); err != nil && oy > 0 { + if err := v.SetOrigin(ox, oy-1); err != nil { + return err + } + } + } + return nil +} + +func pageDown(g *gocui.Gui, v *gocui.View) error { + v, _ = g.View("table") + if v != nil { + _, y := v.Origin() + cx, cy := v.Cursor() + numRows := len(coins) - 1 + _, sy := v.Size() + rows := sy + if (cy + y + rows) > numRows { + // go to last row + v.SetCursor(cx, numRows) + ox, _ := v.Origin() + v.SetOrigin(ox, numRows) + return nil + } + if err := v.SetCursor(cx, cy+rows); err != nil { + ox, oy := v.Origin() + if err := v.SetOrigin(ox, oy+rows); err != nil { + return err + } + } + } + return nil +} + +func pageUp(g *gocui.Gui, v *gocui.View) error { + v, _ = g.View("table") + if v != nil { + ox, oy := v.Origin() + cx, cy := v.Cursor() + _, sy := v.Size() + rows := sy + //fmt.Fprint(v, oy) + if err := v.SetCursor(cx, cy-rows); err != nil && oy > 0 { + if err := v.SetOrigin(ox, oy-rows); err != nil { + return err + } + } + } + return nil +} + +func fetchData() ([]*cmc.Coin, error) { + limit := 100 + result := []*cmc.Coin{} + coins, err := cmc.GetAllCoinData(int(limit)) + if err != nil { + return result, err + } + + for i := range coins { + coin := coins[i] + result = append(result, &coin) + } + + return result, nil +} + +func layout(g *gocui.Gui) error { + maxX, maxY := g.Size() + + chartHeight := 10 + if v, err := g.SetView("chart", 0, 0, maxX, chartHeight); err != nil { + if err != gocui.ErrUnknownView { + return err + } + //v.Frame = false + setChart(g, v) + } + + if v, err := g.SetView("header", 0, chartHeight, maxX, maxY); err != nil { + if err != gocui.ErrUnknownView { + return err + } + t := table.New().SetWidth(maxX) + + headers := []string{ + pad.Right("[r]ank", 13, " "), + pad.Right("[n]ame", 13, " "), + pad.Right("[s]ymbol", 8, " "), + pad.Left("[p]rice", 10, " "), + pad.Left("[m]arket cap", 17, " "), + pad.Left("24H [v]olume", 15, " "), + pad.Left("[1]H%", 9, " "), + pad.Left("[2]4H%", 9, " "), + pad.Left("[7]D%", 9, " "), + pad.Left("[t]otal supply", 20, " "), + pad.Left("[a]vailable supply", 19, " "), + pad.Left("[l]ast updated", 17, " "), + } + for _, h := range headers { + t.AddCol(h) + } + + t.Format().Fprint(v) + v.Highlight = true + v.SelBgColor = gocui.ColorGreen + v.SelFgColor = gocui.ColorBlack + v.Frame = false + } + + if v, err := g.SetView("table", 0, chartHeight+1, maxX, maxY); err != nil { + if err != gocui.ErrUnknownView { + return err + } + _ = v + _ = maxY + setTable(g) + } + return nil +} + +func setChart(g *gocui.Gui, v *gocui.View) error { + v.Frame = false + /* + tm.Clear() + tm.MoveCursor(0, 0) + chart := tm.NewLineChart(maxX/2, 9) + //chart.Flags = tm.DRAW_INDEPENDENT + chart.Flags = tm.DRAW_RELATIVE + data := new(tm.DataTable) + data.AddColumn("") + data.AddColumn("") + + var ( + oneMinute int64 = 60 + oneHour = oneMinute * 60 + oneDay = oneHour * 24 + //oneWeek = oneDay * 7 + oneMonth = oneDay * 30 + //oneYear = oneDay * 365 + ) + + now := time.Now() + secs := now.Unix() + //start := secs - oneDay + start := secs - oneMonth + end := secs + + coin := "ethereum" + graphData, err := cmc.GetCoinGraphData(coin, start, end) + if err != nil { + log.Fatal(err) + } + + var dt []float64 + for i := range graphData.PriceUSD { + dt = append(dt, graphData.PriceUSD[i][1]) + } + + for i, d := range dt { + _ = d + _ = i + data.AddRow(float64(i), d) + } + _ = dt + + out := chart.Draw(data, []int{0, 6}) + fmt.Fprint(v, out) + //tm.Println(out) + var buf bytes.Buffer + tm.Output = bufio.NewWriter(&buf) + //tm.Output = bufio.NewWriter(v) + _ = buf + tm.Flush() + _ = v + //buf.WriteTo(v) + //buf.WriteTo(os.Stdout) + */ + + maxX, _ := g.Size() + if len(points) == 0 { + points = chartCells(maxX, "bitcoin") + } + + for i := range points { + var s string + for j := range points[i] { + p := points[i][j] + s = fmt.Sprintf("%s%c", s, p.Ch) + } + fmt.Fprintln(v, s) + } + return nil +} + +func setTable(g *gocui.Gui) error { + maxX, _ := g.Size() + v, _ := g.View("table") + t := table.New().SetWidth(maxX) + + t.AddCol("") + t.AddCol("") + t.AddCol("") + t.AddCol("") + t.AddCol("") + t.AddCol("") + t.AddCol("") + t.AddCol("") + t.AddCol("") + t.AddCol("") + t.AddCol("") + t.AddCol("") + t.HideColumHeaders = true + + var err error + if len(coins) == 0 { + coins, err = fetchData() + if err != nil { + return err + } + } + for _, coin := range coins { + unix, _ := strconv.ParseInt(coin.LastUpdated, 10, 64) + lastUpdated := time.Unix(unix, 0).Format("15:04:05 Jan 02") + colorprice := cyan + color1h := white + color24h := white + color7d := white + if coin.PercentChange1H > 0 { + color1h = green + } + if coin.PercentChange1H < 0 { + color1h = red + } + if coin.PercentChange24H > 0 { + color24h = green + } + if coin.PercentChange24H < 0 { + color24h = red + } + if coin.PercentChange7D > 0 { + color7d = green + } + if coin.PercentChange7D < 0 { + color7d = red + } + _ = color7d + t.AddRow( + pad.Left(fmt.Sprint(coin.Rank), 4, " "), + pad.Right(coin.Name, 22, " "), + pad.Right(coin.Symbol, 6, " "), + colorprice(pad.Left(humanize.Commaf(coin.PriceUSD), 12, " ")), + pad.Left(humanize.Commaf(coin.MarketCapUSD), 17, " "), + pad.Left(humanize.Commaf(coin.USD24HVolume), 15, " "), + color1h(pad.Left(fmt.Sprintf("%.2f%%", coin.PercentChange1H), 9, " ")), + color24h(pad.Left(fmt.Sprintf("%.2f%%", coin.PercentChange24H), 9, " ")), + color7d(pad.Left(fmt.Sprintf("%.2f%%", coin.PercentChange7D), 9, " ")), + pad.Left(humanize.Commaf(coin.TotalSupply), 20, " "), + pad.Left(humanize.Commaf(coin.AvailableSupply), 18, " "), + pad.Left(fmt.Sprintf("%s", lastUpdated), 18, " "), + // add %percent of cap + ) + } + + //t.SortAsc("Name").SortDesc("Age").Sort().Format().Fprint(v) + t.Format().Fprint(v) + //v.Editable = true + //v.SetCursor(0, 2) + v.Highlight = true + v.SelBgColor = gocui.ColorCyan + v.SelFgColor = gocui.ColorBlack + v.Frame = false + //v.Autoscroll = true + + //buffer := ln.Buffer() + //buffer. + + // Sort by time + // t.SortAscFn("Created", ltTime).Sort().Format().Fprint(v) + return nil +} diff --git a/table/align.go b/table/align.go new file mode 100644 index 0000000..1025d9d --- /dev/null +++ b/table/align.go @@ -0,0 +1,9 @@ +package table + +type Align int + +const ( + AlignLeft = Align(iota) + AlignRight + AlignCenter +) diff --git a/table/align/align.go b/table/align/align.go new file mode 100644 index 0000000..0ec6579 --- /dev/null +++ b/table/align/align.go @@ -0,0 +1,34 @@ +package align + +import ( + "fmt" + "strings" +) + +func AlignLeft(s string, n int) string { + if len(s) > n { + return s[:n] + } + + return fmt.Sprintf("%s%s", s, strings.Repeat(" ", n-len(s))) +} + +func AlignRight(s string, n int) string { + if len(s) > n { + return s[:n] + } + + return fmt.Sprintf("%s%s", strings.Repeat(" ", n-len(s)), s) +} + +func AlignCenter(s string, n int) string { + if len(s) > n { + return s[:n] + } + + pad := (n - len(s)) / 2 + lpad := pad + rpad := n - len(s) - lpad + + return fmt.Sprintf("%s%s%s", strings.Repeat(" ", lpad), s, strings.Repeat(" ", rpad)) +} diff --git a/table/column.go b/table/column.go new file mode 100644 index 0000000..604ae84 --- /dev/null +++ b/table/column.go @@ -0,0 +1,66 @@ +package table + +type FormatFn func(interface{}) string + +type Col struct { + name string + hide bool + format string + formatFn FormatFn + align Align + width int + perc float32 + minWidth int + minWidthPerc int +} + +type Cols []*Col + +func (c *Col) Hide() *Col { + c.hide = true + return c +} + +func (c *Col) SetFormat(f string) *Col { + c.format = f + return c +} + +func (c *Col) SetFormatFn(f FormatFn) *Col { + c.formatFn = f + return c +} + +func (c *Col) AlignLeft() *Col { + c.align = AlignLeft + return c +} + +func (c *Col) AlignRight() *Col { + c.align = AlignRight + return c +} + +func (c *Col) AlignCenter() *Col { + c.align = AlignCenter + return c +} + +func (c *Col) SetWidth(w int) *Col { + c.minWidth = w + return c +} + +func (c *Col) SetWidthPerc(w int) *Col { + c.minWidthPerc = w + return c +} + +func (c Cols) Index(n string) int { + for i := range c { + if c[i].name == n { + return i + } + } + return -1 +} diff --git a/table/row.go b/table/row.go new file mode 100644 index 0000000..08940a4 --- /dev/null +++ b/table/row.go @@ -0,0 +1,64 @@ +package table + +type Row struct { + table *Table + values []interface{} + strValues []string +} + +type Rows []*Row + +func (r Rows) Len() int { + return len(r) +} + +func (r Rows) Swap(i, j int) { + r[i], r[j] = r[j], r[i] +} + +func (r Rows) Less(i, j int) bool { + sortOrder := r[i].table.sort + var k int + for k = 0; k < len(sortOrder)-1; k++ { + s := sortOrder[k] + if s.order == SortDesc { + if gt(r[i].values[s.index], r[j].values[s.index], s.sortFn) { + return true + } + if gt(r[j].values[s.index], r[i].values[s.index], s.sortFn) { + return false + } + } else { + if lt(r[i].values[s.index], r[j].values[s.index], s.sortFn) { + return true + } + if lt(r[j].values[s.index], r[i].values[s.index], s.sortFn) { + return false + } + } + } + + s := sortOrder[k] + if s.order == SortDesc { + return gt(r[i].values[s.index], r[j].values[s.index], s.sortFn) + } + return lt(r[i].values[s.index], r[j].values[s.index], s.sortFn) +} + +func gt(a interface{}, b interface{}, fn SortFn) bool { + return lt(b, a, fn) +} + +func lt(a interface{}, b interface{}, fn SortFn) bool { + if fn != nil { + return fn(a, b) + } else { + switch a.(type) { + case int: + return a.(int) < b.(int) + case string: + return a.(string) < b.(string) + } + } + return false +} diff --git a/table/sort.go b/table/sort.go new file mode 100644 index 0000000..55fe6b1 --- /dev/null +++ b/table/sort.go @@ -0,0 +1,16 @@ +package table + +type SortOrder int +type SortFn func(interface{}, interface{}) bool + +const ( + SortNone SortOrder = iota + SortAsc + SortDesc +) + +type SortBy struct { + index int + order SortOrder + sortFn SortFn +} diff --git a/table/table.go b/table/table.go new file mode 100644 index 0000000..1eee1b6 --- /dev/null +++ b/table/table.go @@ -0,0 +1,214 @@ +package table + +import ( + "fmt" + "io" + "sort" + "strings" + + "github.com/miguelmota/cointop/table/align" +) + +type Table struct { + cols Cols + rows Rows + sort []SortBy + width int + HideColumHeaders bool +} + +func New() *Table { + return &Table{} +} + +func (t *Table) SetWidth(w int) *Table { + t.width = w + return t +} + +func (t *Table) AddCol(n string) *Col { + c := &Col{name: n} + t.cols = append(t.cols, c) + return c +} + +func (t *Table) AddRow(v ...interface{}) *Row { + r := &Row{table: t, values: v, strValues: make([]string, len(v))} + t.rows = append(t.rows, r) + return r +} + +func (t *Table) SortAscFn(n string, fn SortFn) *Table { + i := t.cols.Index(n) + s := SortBy{index: i, order: SortAsc, sortFn: fn} + t.sort = append(t.sort, s) + return t +} + +func (t *Table) SortAsc(n string) *Table { + return t.SortAscFn(n, nil) +} + +func (t *Table) SortDescFn(n string, fn SortFn) *Table { + i := t.cols.Index(n) + s := SortBy{index: i, order: SortDesc, sortFn: fn} + t.sort = append(t.sort, s) + return t +} + +func (t *Table) SortDesc(n string) *Table { + return t.SortDescFn(n, nil) +} + +func (t *Table) Sort() *Table { + if len(t.sort) > 0 { + sort.Sort(t.rows) + } + return t +} + +func (t *Table) colWidth() int { + width := 0 + for _, c := range t.cols { + if c.hide { + continue + } + + width += c.width + } + return width +} + +func (t *Table) normalizeColWidthPerc() { + perc := 0 + for _, c := range t.cols { + if c.hide { + continue + } + + perc += c.minWidthPerc + } + + for _, c := range t.cols { + if c.hide { + continue + } + + c.perc = float32(c.minWidthPerc) / float32(perc) + } +} + +func (t *Table) Format() *Table { + for _, c := range t.cols { + c.width = len(c.name) + 1 + if c.minWidth > c.width { + c.width = c.minWidth + } + } + + for _, r := range t.rows { + for j, v := range r.values { + c := t.cols[j] + + if c.hide { + continue + } + + if c.formatFn != nil { + r.strValues[j] = fmt.Sprintf("%s", c.formatFn(v)) + " " + } else if c.format != "" { + r.strValues[j] = fmt.Sprintf(c.format, v) + " " + } else { + r.strValues[j] = fmt.Sprintf("%v", v) + " " + } + + if len(r.strValues[j]) > t.cols[j].width { + t.cols[j].width = len(r.strValues[j]) + } + } + } + + t.normalizeColWidthPerc() + + unused := t.width - t.colWidth() + if unused <= 0 { + return t + } + + for _, c := range t.cols { + if c.hide { + continue + } + + if c.perc > 0 { + c.width += int(float32(unused) * c.perc) + } + } + + var i int + for i = len(t.cols) - 1; i >= 0; i-- { + if t.cols[i].hide { + continue + } + + break + } + t.cols[i].width += t.width - t.colWidth() + + return t +} + +func (t *Table) Fprint(w io.Writer) { + if !t.HideColumHeaders { + for _, c := range t.cols { + if c.hide { + continue + } + + var s string + switch c.align { + case AlignLeft: + s = align.AlignLeft(c.name+" ", c.width) + case AlignRight: + s = align.AlignRight(c.name+" ", c.width) + case AlignCenter: + s = align.AlignCenter(c.name+" ", c.width) + } + + fmt.Fprintf(w, "%s", s) + } + fmt.Fprintf(w, "\n") + + for _, c := range t.cols { + if c.hide { + continue + } + + fmt.Fprintf(w, strings.Repeat("─", c.width)) + } + fmt.Fprintf(w, "\n") + } + + for _, r := range t.rows { + for i, v := range r.strValues { + c := t.cols[i] + + if c.hide { + continue + } + + var s string + switch c.align { + case AlignLeft: + s = align.AlignLeft(v, c.width) + case AlignRight: + s = align.AlignRight(v, c.width) + case AlignCenter: + s = align.AlignCenter(v, c.width) + } + + fmt.Fprintf(w, "%s", s) + } + fmt.Fprintf(w, "\n") + } +}