pull/5/head
Miguel Mota 6 years ago
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…
Cancel
Save