|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"log"
|
|
|
|
"os/exec"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
humanize "github.com/dustin/go-humanize"
|
|
|
|
"github.com/gizak/termui"
|
|
|
|
"github.com/jroimartin/gocui"
|
|
|
|
"github.com/miguelmota/cointop/apis"
|
|
|
|
apitypes "github.com/miguelmota/cointop/apis/types"
|
|
|
|
"github.com/miguelmota/cointop/color"
|
|
|
|
"github.com/miguelmota/cointop/pad"
|
|
|
|
"github.com/miguelmota/cointop/table"
|
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
oneMinute int64 = 60
|
|
|
|
oneHour = oneMinute * 60
|
|
|
|
oneDay = oneHour * 24
|
|
|
|
oneWeek = oneDay * 7
|
|
|
|
oneMonth = oneDay * 30
|
|
|
|
oneYear = oneDay * 365
|
|
|
|
)
|
|
|
|
|
|
|
|
// Cointop cointop
|
|
|
|
type Cointop struct {
|
|
|
|
g *gocui.Gui
|
|
|
|
chartview *gocui.View
|
|
|
|
chartpoints [][]termui.Cell
|
|
|
|
headersview *gocui.View
|
|
|
|
tableview *gocui.View
|
|
|
|
table *table.Table
|
|
|
|
statusview *gocui.View
|
|
|
|
sortdesc bool
|
|
|
|
currentsort string
|
|
|
|
api apis.Interface
|
|
|
|
coins []*apitypes.Coin
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ct *Cointop) 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
|
|
|
|
}
|
|
|
|
ct.chartview = v
|
|
|
|
ct.chartview.Frame = false
|
|
|
|
ct.updateChart()
|
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
|
|
|
ct.headersview = v
|
|
|
|
ct.headersview.Frame = false
|
|
|
|
ct.headersview.Highlight = true
|
|
|
|
ct.headersview.SelBgColor = gocui.ColorGreen
|
|
|
|
ct.headersview.SelFgColor = gocui.ColorBlack
|
|
|
|
}
|
|
|
|
|
|
|
|
if v, err := g.SetView("table", 0, chartHeight+1, maxX, maxY-1); err != nil {
|
|
|
|
if err != gocui.ErrUnknownView {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
ct.tableview = v
|
|
|
|
ct.tableview.Frame = false
|
|
|
|
ct.tableview.Highlight = true
|
|
|
|
ct.tableview.SelBgColor = gocui.ColorCyan
|
|
|
|
ct.tableview.SelFgColor = gocui.ColorBlack
|
|
|
|
ct.updateTable()
|
|
|
|
ct.rowChanged()
|
|
|
|
}
|
|
|
|
|
|
|
|
if v, err := g.SetView("status", 0, maxY-2, maxX, maxY); err != nil {
|
|
|
|
if err != gocui.ErrUnknownView {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
ct.statusview = v
|
|
|
|
ct.statusview.Frame = false
|
|
|
|
ct.statusview.Highlight = true
|
|
|
|
ct.statusview.SelBgColor = gocui.ColorCyan
|
|
|
|
ct.statusview.SelFgColor = gocui.ColorBlack
|
|
|
|
ct.updateStatus("")
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ct *Cointop) rowChanged() {
|
|
|
|
ct.showLink()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ct *Cointop) fetchData() ([]*apitypes.Coin, error) {
|
|
|
|
limit := 100
|
|
|
|
result := []*apitypes.Coin{}
|
|
|
|
coins, err := ct.api.GetAllCoinData(int(limit))
|
|
|
|
if err != nil {
|
|
|
|
return result, err
|
|
|
|
}
|
|
|
|
|
|
|
|
for i := range coins {
|
|
|
|
coin := coins[i]
|
|
|
|
result = append(result, &coin)
|
|
|
|
}
|
|
|
|
|
|
|
|
return result, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ct *Cointop) chartPoints(maxX int, coin string) error {
|
|
|
|
chart := termui.NewLineChart()
|
|
|
|
chart.Height = 10
|
|
|
|
chart.AxesColor = termui.ColorWhite
|
|
|
|
chart.LineColor = termui.ColorCyan
|
|
|
|
chart.Border = false
|
|
|
|
|
|
|
|
now := time.Now()
|
|
|
|
secs := now.Unix()
|
|
|
|
start := secs - oneDay
|
|
|
|
end := secs
|
|
|
|
|
|
|
|
_ = coin
|
|
|
|
//graphData, err := cmc.GetCoinGraphData(coin, start, end)
|
|
|
|
graphData, err := ct.api.GetGlobalMarketGraphData(start, end)
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
termui.Body = termui.NewGrid()
|
|
|
|
termui.Body.Width = maxX
|
|
|
|
termui.Body.AddRows(
|
|
|
|
termui.NewRow(
|
|
|
|
termui.NewCol(12, 0, chart),
|
|
|
|
),
|
|
|
|
)
|
|
|
|
|
|
|
|
var points [][]termui.Cell
|
|
|
|
// calculate layout
|
|
|
|
termui.Body.Align()
|
|
|
|
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 {
|
|
|
|
p := b.At(j, i)
|
|
|
|
rowpoints = append(rowpoints, p)
|
|
|
|
}
|
|
|
|
points = append(points, rowpoints)
|
|
|
|
}
|
|
|
|
|
|
|
|
ct.chartpoints = points
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ct *Cointop) updateChart() error {
|
|
|
|
maxX, _ := ct.g.Size()
|
|
|
|
if len(ct.chartpoints) == 0 {
|
|
|
|
ct.chartPoints(maxX, "bitcoin")
|
|
|
|
}
|
|
|
|
|
|
|
|
for i := range ct.chartpoints {
|
|
|
|
var s string
|
|
|
|
for j := range ct.chartpoints[i] {
|
|
|
|
p := ct.chartpoints[i][j]
|
|
|
|
s = fmt.Sprintf("%s%c", s, p.Ch)
|
|
|
|
}
|
|
|
|
fmt.Fprintln(ct.chartview, s)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ct *Cointop) updateTable() error {
|
|
|
|
maxX, _ := ct.g.Size()
|
|
|
|
ct.table = table.New().SetWidth(maxX)
|
|
|
|
ct.table.AddCol("")
|
|
|
|
ct.table.AddCol("")
|
|
|
|
ct.table.AddCol("")
|
|
|
|
ct.table.AddCol("")
|
|
|
|
ct.table.AddCol("")
|
|
|
|
ct.table.AddCol("")
|
|
|
|
ct.table.AddCol("")
|
|
|
|
ct.table.AddCol("")
|
|
|
|
ct.table.AddCol("")
|
|
|
|
ct.table.AddCol("")
|
|
|
|
ct.table.AddCol("")
|
|
|
|
ct.table.AddCol("")
|
|
|
|
ct.table.HideColumHeaders = true
|
|
|
|
var err error
|
|
|
|
if len(ct.coins) == 0 {
|
|
|
|
ct.coins, err = ct.fetchData()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for _, coin := range ct.coins {
|
|
|
|
unix, _ := strconv.ParseInt(coin.LastUpdated, 10, 64)
|
|
|
|
lastUpdated := time.Unix(unix, 0).Format("15:04:05 Jan 02")
|
|
|
|
colorprice := color.Cyan
|
|
|
|
color1h := color.White
|
|
|
|
color24h := color.White
|
|
|
|
color7d := color.White
|
|
|
|
if coin.PercentChange1H > 0 {
|
|
|
|
color1h = color.Green
|
|
|
|
}
|
|
|
|
if coin.PercentChange1H < 0 {
|
|
|
|
color1h = color.Red
|
|
|
|
}
|
|
|
|
if coin.PercentChange24H > 0 {
|
|
|
|
color24h = color.Green
|
|
|
|
}
|
|
|
|
if coin.PercentChange24H < 0 {
|
|
|
|
color24h = color.Red
|
|
|
|
}
|
|
|
|
if coin.PercentChange7D > 0 {
|
|
|
|
color7d = color.Green
|
|
|
|
}
|
|
|
|
if coin.PercentChange7D < 0 {
|
|
|
|
color7d = color.Red
|
|
|
|
}
|
|
|
|
ct.table.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
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
ct.table.Format().Fprint(ct.tableview)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ct *Cointop) updateStatus(s string) {
|
|
|
|
maxX, _ := ct.g.Size()
|
|
|
|
ct.statusview.Clear()
|
|
|
|
fmt.Fprintln(ct.statusview, pad.Right(fmt.Sprintf("[q]uit %s", s), maxX, " "))
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ct *Cointop) showLink() {
|
|
|
|
url := ct.rowLink()
|
|
|
|
ct.g.Update(func(g *gocui.Gui) error {
|
|
|
|
ct.updateStatus(fmt.Sprintf("[enter]=%s", url))
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ct *Cointop) enter(g *gocui.Gui, v *gocui.View) error {
|
|
|
|
exec.Command("open", ct.rowLink()).Output()
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ct *Cointop) quit(g *gocui.Gui, v *gocui.View) error {
|
|
|
|
return gocui.ErrQuit
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ct *Cointop) selectedRowIndex() int {
|
|
|
|
_, y := ct.tableview.Origin()
|
|
|
|
_, cy := ct.tableview.Cursor()
|
|
|
|
return y + cy
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ct *Cointop) selectedCoin() *apitypes.Coin {
|
|
|
|
return ct.coins[ct.selectedRowIndex()]
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ct *Cointop) rowLink() string {
|
|
|
|
slug := strings.ToLower(strings.Replace(ct.selectedCoin().Name, " ", "-", -1))
|
|
|
|
return fmt.Sprintf("https://coinmarketcap.com/currencies/%s", slug)
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
ct := Cointop{
|
|
|
|
g: g,
|
|
|
|
api: apis.NewCMC(),
|
|
|
|
}
|
|
|
|
g.SetManagerFunc(ct.layout)
|
|
|
|
|
|
|
|
if err := ct.keybindings(g); err != nil {
|
|
|
|
log.Fatalf("keybindings: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := g.MainLoop(); err != nil && err != gocui.ErrQuit {
|
|
|
|
log.Fatalf("main loop: %v", err)
|
|
|
|
}
|
|
|
|
}
|