mirror of https://github.com/miguelmota/cointop
init
commit
cb8ecd1b3d
@ -0,0 +1,7 @@
|
||||
# cointop
|
||||
|
||||
> Coin tracking for hackers
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
@ -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
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package table
|
||||
|
||||
type Align int
|
||||
|
||||
const (
|
||||
AlignLeft = Align(iota)
|
||||
AlignRight
|
||||
AlignCenter
|
||||
)
|
@ -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))
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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")
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue