mempool-cli/ui/ui.go
2020-02-16 11:40:42 +01:00

302 lines
6.0 KiB
Go

package ui
import (
"fmt"
"math"
"strings"
"github.com/fatih/color"
"github.com/gchaincl/mempool/client"
"github.com/jroimartin/gocui"
)
const (
BLOCK_WIDTH = 22
)
type state struct {
loaded bool
blocks []client.Block
projected []client.ProjectedBlock
vBytesPerSecond int
info *client.MempoolInfo
}
type UI struct {
gui *gocui.Gui
state state
}
func New() (*UI, error) {
gui, err := gocui.NewGui(gocui.Output256)
if err != nil {
return nil, err
}
ui := &UI{gui: gui}
gui.SetManager(ui)
gui.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit)
gui.SetKeybinding("", 'q', gocui.ModNone, quit)
gui.SetKeybinding("", gocui.MouseLeft, gocui.ModNone, ui.click)
gui.Mouse = true
gui.Highlight = true
gui.SelFgColor = gocui.ColorWhite
return ui, nil
}
func quit(*gocui.Gui, *gocui.View) error { return gocui.ErrQuit }
func (ui *UI) Close() { ui.gui.Close() }
func (ui *UI) Loop() error {
err := ui.gui.MainLoop()
// Mask ErrQuit
if err == gocui.ErrQuit {
return nil
}
return err
}
func (ui *UI) Render(resp *client.Response) {
ui.state.loaded = true
ui.state.vBytesPerSecond = resp.VBytesPerSecond
nBlocks := len(resp.Blocks)
blocks := make([]client.Block, nBlocks)
for i := 0; i < nBlocks; i++ {
blocks[i] = resp.Blocks[len(resp.Blocks)-1-i]
}
if bs := blocks; len(bs) != 0 {
ui.state.blocks = bs
}
if bs := resp.ProjectedBlocks; len(bs) != 0 {
ui.state.projected = bs
}
if b := resp.Block; b != nil {
ui.state.blocks = append([]client.Block{*b}, ui.state.blocks...)
}
if info := resp.MempoolInfo; info != nil {
ui.state.info = info
}
// delete all the views
for _, v := range ui.gui.Views() {
ui.gui.DeleteView(v.Name())
}
ui.gui.Update(ui.Layout)
}
func (ui *UI) Layout(g *gocui.Gui) error {
x, y := g.Size()
if !ui.state.loaded {
return ui.loading(g, x, y)
}
g.DeleteView("loading")
// vertical layout is used if 8 blocks don't fit on the screen
// when in vertical layout the mempool is shown in the top
// and the blockchain in the bottom
vertical := BLOCK_WIDTH*6 > x
// draw projected blocks (mempool)
for i, _ := range ui.state.projected {
name := fmt.Sprintf("projected-block-%d", i)
var x0, x1, y0, y1 int
if vertical {
x0 = x - (BLOCK_WIDTH+2)*(i+1)
x1 = x0 + BLOCK_WIDTH
y0 = (y / 2) - 12
y1 = (y / 2) - 2
} else {
x0 = x/2 - BLOCK_WIDTH*(i+1)
x1 = x0 + BLOCK_WIDTH - 2
y0 = (y / 2) - 5
y1 = (y / 2) + 5
}
v, err := g.SetView(name, x0, y0, x1, y1)
if err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.BgColor = gocui.ColorBlack
}
v.Clear()
if _, err := v.Write(ui.printProjectedBlock(i, x1-x0, y1-y0)); err != nil {
return err
}
}
if err := ui.separator(g, x, y, vertical); err != nil {
return err
}
// draw blockchain blocks
for i, block := range ui.state.blocks {
name := fmt.Sprintf("block-%d", i)
var x0, x1, y0, y1 int
if vertical {
x0 = x - (BLOCK_WIDTH+2)*(i+1)
x1 = x0 + BLOCK_WIDTH
y0 = (y / 2) + 2
y1 = (y / 2) + 12
} else {
x0 = (x / 2) + (BLOCK_WIDTH*i + 1) + 1
x1 = x0 + BLOCK_WIDTH - 2
y0 = (y / 2) - 5
y1 = (y / 2) + 5
}
v, err := g.SetView(name, x0, y0, x1, y1)
if err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.BgColor = gocui.ColorBlack
}
v.Title = fmt.Sprintf("#%d", block.Height)
v.Clear()
if _, err := v.Write(ui.printBlock(i, x1-x0, y1-y0)); err != nil {
return err
}
}
if err := ui.info(g, x, y); err != nil {
return err
}
return nil
}
func (ui *UI) loading(g *gocui.Gui, x, y int) error {
v, err := g.SetView("loading", x/2-10, y/2-1, x/2+10, y/2+1)
if err != nil {
if err != gocui.ErrUnknownView {
return err
}
}
v.Clear()
fmt.Fprintf(v, "Loading blocks ...")
return nil
}
func (ui *UI) printProjectedBlock(n int, x, y int) []byte {
b := ui.state.projected[n]
return ProjectedBlock(b).Print(n, x, y)
}
func (ui *UI) printBlock(n int, x, y int) []byte {
b := ui.state.blocks[n]
return Block(b).Print(n, x, y)
}
func (ui *UI) separator(g *gocui.Gui, x, y int, vertical bool) error {
var x0, x1, y0, y1 int
if vertical {
x0, x1 = 0, x
y0, y1 = y/2-1, y/2+1
} else {
x0, x1 = x/2-1, x/2+1
y0, y1 = 0, y
}
v, err := g.SetView("separator", x0, y0, x1, y1)
if err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.Frame = false
v.Wrap = true
}
v.Clear()
if vertical {
fmt.Fprintf(v, strings.Repeat("-", x))
} else {
fmt.Fprintf(v, strings.Repeat("|", y))
}
return nil
}
func (ui *UI) info(g *gocui.Gui, x, y int) error {
var (
white = color.New(color.FgWhite).SprintfFunc()
red = color.New(color.FgRed).SprintfFunc()
blue = color.New(color.FgBlue).SprintfFunc()
)
v, err := g.SetView("info", 0, y-2, x, y)
if err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.Frame = false
v.BgColor = gocui.ColorBlack
}
v.Clear()
info := ui.state.info
if info == nil {
return nil
}
var mSize int
for _, b := range ui.state.projected {
mSize += b.BlockSize
}
// Compute the total number of blocks on the mempool
// We use the total BlockWeight / 4mm
var w float64
for _, b := range ui.state.projected {
w += float64(b.BlockWeight)
}
fmt.Fprintf(v, "%s %s, %s %s, %s %s",
red("Unconfirmed Txs: "), white("%d", info.Size),
blue("Mempool size"), white("%s (%d block/s)", fmtSize(mSize), ceil(w/4_000_000)),
blue("Tx weight per second"), red("%d", ui.state.vBytesPerSecond),
)
return nil
}
func ceil(f float64) int {
return int(
math.Ceil(f),
)
}
func fmtSeconds(s int64) string {
if s < 60 {
return "< 1 minute"
} else if s < 120 {
return fmt.Sprintf("1 minute")
} else if s < 3600 {
return fmt.Sprintf("%d minutes", s/60)
}
return fmt.Sprintf("%d hours", s/3600)
}
func fmtSize(s int) string {
if s < 1024*1024 {
m := float64(s) / 1000.0
return fmt.Sprintf("%dkB", ceil(m))
}
m := float64(s) / (1000.0 * 1000.0)
return fmt.Sprintf("%dMB", ceil(m))
}
func (ui *UI) click(g *gocui.Gui, v *gocui.View) error {
return nil
}