mirror of https://github.com/edouardparis/lntop
V0.1.0 (#10)
* config: add Views * refac views channels * fix config default * views: menu * config default: add comment * fix config default * README: enhance config section * fix README * controller: F2 Menu * views: transactions * fix views: set current in layout * fix menu * ft transactions * fix Help * fix controller Menu * view: transaction * refac controller * fix ui controller * try some bold * fix cursor * refac color * focus column transactions * controller: add keyBinding Menu m * help view: add menu * fix cursor: push to the right * refac remove current model * ui: txs and channels sortable * fix focus column * view transaction: transaction dest addresses * fix menu * refac controller * channels: sort * rename current column * refac cursor * refac currentColumnIndex * set cursor if view deleted * remove previous * clean view.View * controller: ToggleView * fix menu * view txs: add config * feat order * fix channels sort * feat transactions sort * feat help: add asc/desc * fix README * color: magenta * fix color * fix views menu * fix views help * network backend: SubscribeTransactions * pubsub: transactions * controller.Listen: refresh transactions * fix controller * fix controller pubsub: no need for wallet ticker * fix models transactions * views channels: column SENT and RECEIVED * update version * fix README * fix README * fix models sort * fix readme and default config * fix readmepull/21/head v0.1.0
parent
314908296a
commit
f72c5ca099
@ -0,0 +1,22 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
type Transaction struct {
|
||||
// / The transaction hash
|
||||
TxHash string
|
||||
// / The transaction amount, denominated in satoshis
|
||||
Amount int64
|
||||
// / The number of confirmations
|
||||
NumConfirmations int32
|
||||
// / The hash of the block this transaction was included in
|
||||
BlockHash string
|
||||
// / The height of the block this transaction was included in
|
||||
BlockHeight int32
|
||||
// / Timestamp of this transaction
|
||||
Date time.Time
|
||||
// / Fees paid for this transaction
|
||||
TotalFees int64
|
||||
// / Addresses that received funds for this transaction
|
||||
DestAddresses []string
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
package cursor
|
||||
|
||||
type View interface {
|
||||
Cursor() (int, int)
|
||||
Origin() (int, int)
|
||||
Speed() (int, int, int, int)
|
||||
SetCursor(int, int) error
|
||||
SetOrigin(int, int) error
|
||||
}
|
||||
|
||||
func Down(v View) error {
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
cx, cy := v.Cursor()
|
||||
_, _, sy, _ := v.Speed()
|
||||
err := v.SetCursor(cx, cy+sy)
|
||||
if err != nil {
|
||||
ox, oy := v.Origin()
|
||||
err := v.SetOrigin(ox, oy+sy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Up(v View) error {
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
ox, oy := v.Origin()
|
||||
cx, cy := v.Cursor()
|
||||
_, _, _, sy := v.Speed()
|
||||
err := v.SetCursor(cx, cy-sy)
|
||||
if err != nil && oy >= sy {
|
||||
err := v.SetOrigin(ox, oy-sy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Right(v View) error {
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
cx, cy := v.Cursor()
|
||||
sx, _, _, _ := v.Speed()
|
||||
err := v.SetCursor(cx+sx, cy)
|
||||
if err != nil {
|
||||
ox, oy := v.Origin()
|
||||
err := v.SetOrigin(ox+sx, oy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Left(v View) error {
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
ox, oy := v.Origin()
|
||||
cx, cy := v.Cursor()
|
||||
_, sx, _, _ := v.Speed()
|
||||
err := v.SetCursor(cx-sx, cy)
|
||||
if err != nil {
|
||||
err := v.SetCursor(0, cy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if ox >= sx-cx {
|
||||
err := v.SetOrigin(ox-sx+cx, oy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"github.com/edouardparis/lntop/ui/models"
|
||||
"github.com/jroimartin/gocui"
|
||||
)
|
||||
|
||||
func quit(g *gocui.Gui, v *gocui.View) error {
|
||||
return gocui.ErrQuit
|
||||
}
|
||||
|
||||
func setKeyBinding(c *controller, g *gocui.Gui) error {
|
||||
err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = g.SetKeybinding("", gocui.KeyF10, gocui.ModNone, quit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = g.SetKeybinding("", 'q', gocui.ModNone, quit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = g.SetKeybinding("", gocui.KeyArrowUp, gocui.ModNone, c.cursorUp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = g.SetKeybinding("", gocui.KeyArrowDown, gocui.ModNone, c.cursorDown)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = g.SetKeybinding("", gocui.KeyArrowLeft, gocui.ModNone, c.cursorLeft)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = g.SetKeybinding("", gocui.KeyArrowRight, gocui.ModNone, c.cursorRight)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = g.SetKeybinding("", gocui.KeyEnter, gocui.ModNone, c.OnEnter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = g.SetKeybinding("", gocui.KeyF1, gocui.ModNone, c.Help)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = g.SetKeybinding("", 'h', gocui.ModNone, c.Help)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = g.SetKeybinding("", gocui.KeyF2, gocui.ModNone, c.Menu)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = g.SetKeybinding("", 'm', gocui.ModNone, c.Menu)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = g.SetKeybinding("", 'a', gocui.ModNone, c.Order(models.Asc))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = g.SetKeybinding("", 'd', gocui.ModNone, c.Order(models.Desc))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
type Order int
|
||||
|
||||
const (
|
||||
Asc Order = iota
|
||||
Desc
|
||||
)
|
||||
|
||||
func IntSort(a, b int, o Order) bool {
|
||||
if o == Asc {
|
||||
return a < b
|
||||
}
|
||||
return a > b
|
||||
}
|
||||
|
||||
func Int32Sort(a, b int32, o Order) bool {
|
||||
if o == Asc {
|
||||
return a < b
|
||||
}
|
||||
return a > b
|
||||
}
|
||||
|
||||
func Int64Sort(a, b int64, o Order) bool {
|
||||
if o == Asc {
|
||||
return a < b
|
||||
}
|
||||
return a > b
|
||||
}
|
||||
|
||||
func DateSort(a, b *time.Time, o Order) bool {
|
||||
if o == Desc {
|
||||
if a == nil || b == nil {
|
||||
return b == nil
|
||||
}
|
||||
|
||||
return a.After(*b)
|
||||
}
|
||||
|
||||
if a == nil || b == nil {
|
||||
return a == nil
|
||||
}
|
||||
|
||||
return a.Before(*b)
|
||||
}
|
@ -0,0 +1,119 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"github.com/edouardparis/lntop/network/models"
|
||||
)
|
||||
|
||||
type TransactionsSort func(*models.Transaction, *models.Transaction) bool
|
||||
|
||||
type Transactions struct {
|
||||
current *models.Transaction
|
||||
list []*models.Transaction
|
||||
sort TransactionsSort
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func (t *Transactions) Current() *models.Transaction {
|
||||
return t.current
|
||||
}
|
||||
|
||||
func (t *Transactions) SetCurrent(index int) {
|
||||
t.current = t.Get(index)
|
||||
}
|
||||
|
||||
func (t *Transactions) List() []*models.Transaction {
|
||||
return t.list
|
||||
}
|
||||
|
||||
func (t *Transactions) Len() int {
|
||||
return len(t.list)
|
||||
}
|
||||
|
||||
func (t *Transactions) Swap(i, j int) {
|
||||
t.list[i], t.list[j] = t.list[j], t.list[i]
|
||||
}
|
||||
|
||||
func (t *Transactions) Less(i, j int) bool {
|
||||
return t.sort(t.list[i], t.list[j])
|
||||
}
|
||||
|
||||
func (t *Transactions) Sort(s TransactionsSort) {
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
t.sort = s
|
||||
sort.Sort(t)
|
||||
}
|
||||
|
||||
func (t *Transactions) Get(index int) *models.Transaction {
|
||||
if index < 0 || index > len(t.list)-1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return t.list[index]
|
||||
}
|
||||
|
||||
func (t *Transactions) Contains(tx *models.Transaction) bool {
|
||||
if tx == nil {
|
||||
return false
|
||||
}
|
||||
for i := range t.list {
|
||||
if t.list[i].TxHash == tx.TxHash {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (t *Transactions) Add(tx *models.Transaction) {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
if t.Contains(tx) {
|
||||
return
|
||||
}
|
||||
t.list = append(t.list, tx)
|
||||
if t.sort != nil {
|
||||
sort.Sort(t)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Transactions) Update(tx *models.Transaction) {
|
||||
if tx == nil {
|
||||
return
|
||||
}
|
||||
if !t.Contains(tx) {
|
||||
t.Add(tx)
|
||||
return
|
||||
}
|
||||
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
|
||||
for i := range t.list {
|
||||
if t.list[i].TxHash == tx.TxHash {
|
||||
t.list[i].NumConfirmations = tx.NumConfirmations
|
||||
t.list[i].BlockHeight = tx.BlockHeight
|
||||
}
|
||||
}
|
||||
|
||||
if t.sort != nil {
|
||||
sort.Sort(t)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Models) RefreshTransactions(ctx context.Context) error {
|
||||
transactions, err := m.network.GetTransactions(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i := range transactions {
|
||||
m.Transactions.Update(transactions[i])
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
package views
|
||||
|
||||
import "github.com/jroimartin/gocui"
|
||||
|
||||
func cursorDown(v *gocui.View, speed int) error {
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
cx, cy := v.Cursor()
|
||||
err := v.SetCursor(cx, cy+speed)
|
||||
if err != nil {
|
||||
ox, oy := v.Origin()
|
||||
err := v.SetOrigin(ox, oy+speed)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func cursorUp(v *gocui.View, speed int) error {
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
ox, oy := v.Origin()
|
||||
cx, cy := v.Cursor()
|
||||
err := v.SetCursor(cx, cy-speed)
|
||||
if err != nil && oy >= speed {
|
||||
err := v.SetOrigin(ox, oy-speed)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func cursorRight(v *gocui.View, speed int) error {
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
cx, cy := v.Cursor()
|
||||
err := v.SetCursor(cx+speed, cy)
|
||||
if err != nil {
|
||||
ox, oy := v.Origin()
|
||||
err := v.SetOrigin(ox+speed, oy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func cursorLeft(v *gocui.View, speed int) error {
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
ox, oy := v.Origin()
|
||||
cx, cy := v.Cursor()
|
||||
err := v.SetCursor(cx-speed, cy)
|
||||
if err != nil && ox >= speed {
|
||||
err := v.SetOrigin(ox-speed, oy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
@ -0,0 +1,162 @@
|
||||
package views
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/edouardparis/lntop/ui/color"
|
||||
"github.com/jroimartin/gocui"
|
||||
)
|
||||
|
||||
const (
|
||||
MENU = "menu"
|
||||
MENU_HEADER = "menu_header"
|
||||
MENU_FOOTER = "menu_footer"
|
||||
)
|
||||
|
||||
var menu = []string{
|
||||
"CHANNEL",
|
||||
"TRANSAC",
|
||||
}
|
||||
|
||||
type Menu struct {
|
||||
view *gocui.View
|
||||
|
||||
cy, oy int
|
||||
}
|
||||
|
||||
func (h Menu) Name() string {
|
||||
return MENU
|
||||
}
|
||||
|
||||
func (h *Menu) Wrap(v *gocui.View) View {
|
||||
h.view = v
|
||||
return h
|
||||
}
|
||||
|
||||
func (h Menu) Origin() (int, int) {
|
||||
return 0, h.oy
|
||||
}
|
||||
|
||||
func (h Menu) Cursor() (int, int) {
|
||||
return 0, h.cy
|
||||
}
|
||||
|
||||
func (h Menu) Speed() (int, int, int, int) {
|
||||
return 0, 0, 1, 1
|
||||
}
|
||||
|
||||
func (h *Menu) SetCursor(x, y int) error {
|
||||
err := h.view.SetCursor(x, y)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
h.cy = y
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Menu) SetOrigin(x, y int) error {
|
||||
err := h.view.SetOrigin(x, y)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
h.oy = y
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h Menu) Current() string {
|
||||
_, y := h.view.Cursor()
|
||||
if y < len(menu) {
|
||||
switch menu[y] {
|
||||
case "CHANNEL":
|
||||
return CHANNELS
|
||||
case "TRANSAC":
|
||||
return TRANSACTIONS
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c Menu) Delete(g *gocui.Gui) error {
|
||||
err := g.DeleteView(MENU_HEADER)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = g.DeleteView(MENU_FOOTER)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return g.DeleteView(MENU)
|
||||
}
|
||||
|
||||
func (h Menu) Set(g *gocui.Gui, x0, y0, x1, y1 int) error {
|
||||
setCursor := false
|
||||
header, err := g.SetView(MENU_HEADER, x0-1, y0, x1, y0+2)
|
||||
if err != nil {
|
||||
if err != gocui.ErrUnknownView {
|
||||
return err
|
||||
}
|
||||
setCursor = true
|
||||
}
|
||||
header.Frame = false
|
||||
header.BgColor = gocui.ColorGreen
|
||||
header.FgColor = gocui.ColorBlack
|
||||
|
||||
header.Clear()
|
||||
fmt.Fprintln(header, " MENU")
|
||||
|
||||
h.view, err = g.SetView(MENU, x0-1, y0+1, x1, y1-2)
|
||||
if err != nil {
|
||||
if err != gocui.ErrUnknownView {
|
||||
return err
|
||||
}
|
||||
setCursor = true
|
||||
}
|
||||
|
||||
h.view.Frame = false
|
||||
h.view.Highlight = true
|
||||
h.view.SelBgColor = gocui.ColorCyan
|
||||
h.view.SelFgColor = gocui.ColorBlack
|
||||
if setCursor {
|
||||
ox, oy := h.Origin()
|
||||
err := h.SetOrigin(ox, oy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cx, cy := h.Cursor()
|
||||
err = h.SetCursor(cx, cy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
h.view.Clear()
|
||||
for i := range menu {
|
||||
fmt.Fprintln(h.view, fmt.Sprintf(" %-9s", menu[i]))
|
||||
}
|
||||
_, err = g.SetCurrentView(MENU)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
footer, err := g.SetView(MENU_FOOTER, x0-1, y1-2, x1, y1)
|
||||
if err != nil {
|
||||
if err != gocui.ErrUnknownView {
|
||||
return err
|
||||
}
|
||||
}
|
||||
footer.Frame = false
|
||||
footer.BgColor = gocui.ColorCyan
|
||||
footer.FgColor = gocui.ColorBlack
|
||||
footer.Clear()
|
||||
blackBg := color.Black(color.Background)
|
||||
fmt.Fprintln(footer, fmt.Sprintf("%s%s",
|
||||
blackBg("F2"), "Close",
|
||||
))
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewMenu() *Menu { return &Menu{} }
|
@ -0,0 +1,148 @@
|
||||
package views
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jroimartin/gocui"
|
||||
"golang.org/x/text/language"
|
||||
"golang.org/x/text/message"
|
||||
|
||||
"github.com/edouardparis/lntop/ui/color"
|
||||
"github.com/edouardparis/lntop/ui/models"
|
||||
)
|
||||
|
||||
const (
|
||||
TRANSACTION = "transaction"
|
||||
TRANSACTION_HEADER = "transaction_header"
|
||||
TRANSACTION_FOOTER = "transaction_footer"
|
||||
)
|
||||
|
||||
type Transaction struct {
|
||||
view *gocui.View
|
||||
transactions *models.Transactions
|
||||
}
|
||||
|
||||
func (c Transaction) Name() string {
|
||||
return TRANSACTION
|
||||
}
|
||||
|
||||
func (c Transaction) Empty() bool {
|
||||
return c.transactions == nil
|
||||
}
|
||||
|
||||
func (c *Transaction) Wrap(v *gocui.View) View {
|
||||
c.view = v
|
||||
return c
|
||||
}
|
||||
|
||||
func (c Transaction) Origin() (int, int) {
|
||||
return c.view.Origin()
|
||||
}
|
||||
|
||||
func (c Transaction) Cursor() (int, int) {
|
||||
return c.view.Cursor()
|
||||
}
|
||||
|
||||
func (c Transaction) Speed() (int, int, int, int) {
|
||||
return 1, 1, 1, 1
|
||||
}
|
||||
|
||||
func (c *Transaction) SetCursor(x, y int) error {
|
||||
return c.view.SetCursor(x, y)
|
||||
}
|
||||
|
||||
func (c *Transaction) SetOrigin(x, y int) error {
|
||||
return c.view.SetOrigin(x, y)
|
||||
}
|
||||
|
||||
func (c *Transaction) Set(g *gocui.Gui, x0, y0, x1, y1 int) error {
|
||||
header, err := g.SetView(TRANSACTION_HEADER, x0-1, y0, x1+2, y0+2)
|
||||
if err != nil {
|
||||
if err != gocui.ErrUnknownView {
|
||||
return err
|
||||
}
|
||||
}
|
||||
header.Frame = false
|
||||
header.BgColor = gocui.ColorGreen
|
||||
header.FgColor = gocui.ColorBlack | gocui.AttrBold
|
||||
header.Clear()
|
||||
fmt.Fprintln(header, "Transaction")
|
||||
|
||||
v, err := g.SetView(TRANSACTION, x0-1, y0+1, x1+2, y1-1)
|
||||
if err != nil {
|
||||
if err != gocui.ErrUnknownView {
|
||||
return err
|
||||
}
|
||||
}
|
||||
v.Frame = false
|
||||
c.view = v
|
||||
c.display()
|
||||
|
||||
footer, err := g.SetView(TRANSACTION_FOOTER, x0-1, y1-2, x1, y1)
|
||||
if err != nil {
|
||||
if err != gocui.ErrUnknownView {
|
||||
return err
|
||||
}
|
||||
}
|
||||
footer.Frame = false
|
||||
footer.BgColor = gocui.ColorCyan
|
||||
footer.FgColor = gocui.ColorBlack
|
||||
footer.Clear()
|
||||
blackBg := color.Black(color.Background)
|
||||
fmt.Fprintln(footer, fmt.Sprintf("%s%s %s%s %s%s %s%s",
|
||||
blackBg("F1"), "Help",
|
||||
blackBg("F2"), "Menu",
|
||||
blackBg("Enter"), "Transactions",
|
||||
blackBg("F10"), "Quit",
|
||||
))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Transaction) Delete(g *gocui.Gui) error {
|
||||
err := g.DeleteView(TRANSACTION_HEADER)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = g.DeleteView(TRANSACTION)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return g.DeleteView(TRANSACTION_FOOTER)
|
||||
}
|
||||
|
||||
func (c *Transaction) display() {
|
||||
p := message.NewPrinter(language.English)
|
||||
v := c.view
|
||||
v.Clear()
|
||||
transaction := c.transactions.Current()
|
||||
green := color.Green()
|
||||
cyan := color.Cyan()
|
||||
fmt.Fprintln(v, green(" [ Transaction ]"))
|
||||
fmt.Fprintln(v, fmt.Sprintf("%s %s",
|
||||
cyan(" Date:"), transaction.Date.Format("15:04:05 Jan _2")))
|
||||
fmt.Fprintln(v, p.Sprintf("%s %d",
|
||||
cyan(" Amount:"), transaction.Amount))
|
||||
fmt.Fprintln(v, p.Sprintf("%s %d",
|
||||
cyan(" Fee:"), transaction.TotalFees))
|
||||
fmt.Fprintln(v, p.Sprintf("%s %d",
|
||||
cyan(" BlockHeight:"), transaction.BlockHeight))
|
||||
fmt.Fprintln(v, p.Sprintf("%s %d",
|
||||
cyan("NumConfirmations:"), transaction.NumConfirmations))
|
||||
fmt.Fprintln(v, p.Sprintf("%s %s",
|
||||
cyan(" BlockHash:"), transaction.BlockHash))
|
||||
fmt.Fprintln(v, fmt.Sprintf("%s %s",
|
||||
cyan(" TxHash:"), transaction.TxHash))
|
||||
fmt.Fprintln(v, "")
|
||||
fmt.Fprintln(v, green("[ addresses ]"))
|
||||
for i := range transaction.DestAddresses {
|
||||
fmt.Fprintln(v, fmt.Sprintf("%s %s",
|
||||
cyan(" -"), transaction.DestAddresses[i]))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func NewTransaction(transactions *models.Transactions) *Transaction {
|
||||
return &Transaction{transactions: transactions}
|
||||
}
|
@ -0,0 +1,384 @@
|
||||
package views
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"github.com/jroimartin/gocui"
|
||||
"golang.org/x/text/language"
|
||||
"golang.org/x/text/message"
|
||||
|
||||
"github.com/edouardparis/lntop/config"
|
||||
netmodels "github.com/edouardparis/lntop/network/models"
|
||||
"github.com/edouardparis/lntop/ui/color"
|
||||
"github.com/edouardparis/lntop/ui/models"
|
||||
)
|
||||
|
||||
const (
|
||||
TRANSACTIONS = "transactions"
|
||||
TRANSACTIONS_COLUMNS = "transactions_columns"
|
||||
TRANSACTIONS_FOOTER = "transactions_footer"
|
||||
)
|
||||
|
||||
var DefaultTransactionsColumns = []string{
|
||||
"DATE",
|
||||
"HEIGHT",
|
||||
"CONFIR",
|
||||
"AMOUNT",
|
||||
"FEE",
|
||||
"ADDRESSES",
|
||||
}
|
||||
|
||||
type Transactions struct {
|
||||
cfg *config.View
|
||||
|
||||
columns []transactionsColumn
|
||||
columnsView *gocui.View
|
||||
view *gocui.View
|
||||
transactions *models.Transactions
|
||||
|
||||
ox, oy int
|
||||
cx, cy int
|
||||
}
|
||||
|
||||
type transactionsColumn struct {
|
||||
name string
|
||||
width int
|
||||
sorted bool
|
||||
sort func(models.Order) models.TransactionsSort
|
||||
display func(*netmodels.Transaction, ...color.Option) string
|
||||
}
|
||||
|
||||
func (c Transactions) Index() int {
|
||||
_, oy := c.view.Origin()
|
||||
_, cy := c.view.Cursor()
|
||||
return cy + oy
|
||||
}
|
||||
|
||||
func (c Transactions) Name() string {
|
||||
return TRANSACTIONS
|
||||
}
|
||||
|
||||
func (c *Transactions) Wrap(v *gocui.View) View {
|
||||
c.view = v
|
||||
return c
|
||||
}
|
||||
|
||||
func (c Transactions) currentColumnIndex() int {
|
||||
x := c.ox + c.cx
|
||||
index := 0
|
||||
sum := 0
|
||||
for i := range c.columns {
|
||||
sum += c.columns[i].width + 1
|
||||
if x < sum {
|
||||
return index
|
||||
}
|
||||
index++
|
||||
}
|
||||
return index
|
||||
}
|
||||
|
||||
func (c Transactions) Origin() (int, int) {
|
||||
return c.ox, c.oy
|
||||
}
|
||||
|
||||
func (c Transactions) Cursor() (int, int) {
|
||||
return c.cx, c.cy
|
||||
}
|
||||
|
||||
func (c *Transactions) SetCursor(cx, cy int) error {
|
||||
err := c.columnsView.SetCursor(cx, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = c.view.SetCursor(cx, cy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.cx, c.cy = cx, cy
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Transactions) SetOrigin(ox, oy int) error {
|
||||
err := c.columnsView.SetOrigin(ox, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = c.view.SetOrigin(ox, oy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.ox, c.oy = ox, oy
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Transactions) Speed() (int, int, int, int) {
|
||||
current := c.currentColumnIndex()
|
||||
if current > len(c.columns)-1 {
|
||||
return 0, c.columns[current-1].width + 1, 1, 1
|
||||
}
|
||||
if current == 0 {
|
||||
return c.columns[0].width + 1, 0, 1, 1
|
||||
}
|
||||
return c.columns[current].width + 1,
|
||||
c.columns[current-1].width + 1,
|
||||
1, 1
|
||||
}
|
||||
|
||||
func (c *Transactions) Sort(column string, order models.Order) {
|
||||
if column == "" {
|
||||
index := c.currentColumnIndex()
|
||||
col := c.columns[index]
|
||||
if col.sort == nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.transactions.Sort(col.sort(order))
|
||||
for i := range c.columns {
|
||||
c.columns[i].sorted = (i == index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c Transactions) Delete(g *gocui.Gui) error {
|
||||
err := g.DeleteView(TRANSACTIONS_COLUMNS)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = g.DeleteView(TRANSACTIONS)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return g.DeleteView(TRANSACTIONS_FOOTER)
|
||||
}
|
||||
|
||||
func (c *Transactions) Set(g *gocui.Gui, x0, y0, x1, y1 int) error {
|
||||
var err error
|
||||
setCursor := false
|
||||
c.columnsView, err = g.SetView(TRANSACTIONS_COLUMNS, x0-1, y0, x1+2, y0+2)
|
||||
if err != nil {
|
||||
if err != gocui.ErrUnknownView {
|
||||
return err
|
||||
}
|
||||
setCursor = true
|
||||
}
|
||||
c.columnsView.Frame = false
|
||||
c.columnsView.BgColor = gocui.ColorGreen
|
||||
c.columnsView.FgColor = gocui.ColorBlack
|
||||
|
||||
c.view, err = g.SetView(TRANSACTIONS, x0-1, y0+1, x1+2, y1-1)
|
||||
if err != nil {
|
||||
if err != gocui.ErrUnknownView {
|
||||
return err
|
||||
}
|
||||
setCursor = true
|
||||
}
|
||||
c.view.Frame = false
|
||||
c.view.Autoscroll = false
|
||||
c.view.SelBgColor = gocui.ColorCyan
|
||||
c.view.SelFgColor = gocui.ColorBlack
|
||||
c.view.Highlight = true
|
||||
if setCursor {
|
||||
ox, oy := c.Origin()
|
||||
err := c.SetOrigin(ox, oy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cx, cy := c.Cursor()
|
||||
err = c.SetCursor(cx, cy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
c.display()
|
||||
|
||||
footer, err := g.SetView(TRANSACTIONS_FOOTER, x0-1, y1-2, x1+2, y1)
|
||||
if err != nil {
|
||||
if err != gocui.ErrUnknownView {
|
||||
return err
|
||||
}
|
||||
}
|
||||
footer.Frame = false
|
||||
footer.BgColor = gocui.ColorCyan
|
||||
footer.FgColor = gocui.ColorBlack
|
||||
footer.Clear()
|
||||
blackBg := color.Black(color.Background)
|
||||
fmt.Fprintln(footer, fmt.Sprintf("%s%s %s%s %s%s %s%s",
|
||||
blackBg("F1"), "Help",
|
||||
blackBg("F2"), "Menu",
|
||||
blackBg("Enter"), "Transaction",
|
||||
blackBg("F10"), "Quit",
|
||||
))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Transactions) display() {
|
||||
c.columnsView.Clear()
|
||||
var buffer bytes.Buffer
|
||||
current := c.currentColumnIndex()
|
||||
for i := range c.columns {
|
||||
if current == i {
|
||||
buffer.WriteString(color.Cyan(color.Background)(c.columns[i].name))
|
||||
buffer.WriteString(" ")
|
||||
continue
|
||||
} else if c.columns[i].sorted {
|
||||
buffer.WriteString(color.Magenta(color.Background)(c.columns[i].name))
|
||||
buffer.WriteString(" ")
|
||||
continue
|
||||
}
|
||||
buffer.WriteString(c.columns[i].name)
|
||||
buffer.WriteString(" ")
|
||||
}
|
||||
fmt.Fprintln(c.columnsView, buffer.String())
|
||||
|
||||
c.view.Clear()
|
||||
for _, item := range c.transactions.List() {
|
||||
var buffer bytes.Buffer
|
||||
for i := range c.columns {
|
||||
var opt color.Option
|
||||
if current == i {
|
||||
opt = color.Bold
|
||||
}
|
||||
buffer.WriteString(c.columns[i].display(item, opt))
|
||||
buffer.WriteString(" ")
|
||||
}
|
||||
fmt.Fprintln(c.view, buffer.String())
|
||||
}
|
||||
}
|
||||
|
||||
func NewTransactions(cfg *config.View, txs *models.Transactions) *Transactions {
|
||||
transactions := &Transactions{
|
||||
cfg: cfg,
|
||||
transactions: txs,
|
||||
}
|
||||
|
||||
printer := message.NewPrinter(language.English)
|
||||
|
||||
columns := DefaultTransactionsColumns
|
||||
if cfg != nil && len(cfg.Columns) != 0 {
|
||||
columns = cfg.Columns
|
||||
}
|
||||
|
||||
transactions.columns = make([]transactionsColumn, len(columns))
|
||||
|
||||
for i := range columns {
|
||||
switch columns[i] {
|
||||
case "DATE":
|
||||
transactions.columns[i] = transactionsColumn{
|
||||
name: fmt.Sprintf("%-15s", columns[i]),
|
||||
width: 15,
|
||||
sort: func(order models.Order) models.TransactionsSort {
|
||||
return func(tx1, tx2 *netmodels.Transaction) bool {
|
||||
return models.DateSort(&tx1.Date, &tx2.Date, order)
|
||||
}
|
||||
},
|
||||
display: func(tx *netmodels.Transaction, opts ...color.Option) string {
|
||||
return color.Cyan(opts...)(
|
||||
fmt.Sprintf("%15s", tx.Date.Format("15:04:05 Jan _2")),
|
||||
)
|
||||
},
|
||||
}
|
||||
case "HEIGHT":
|
||||
transactions.columns[i] = transactionsColumn{
|
||||
name: fmt.Sprintf("%8s", columns[i]),
|
||||
width: 8,
|
||||
sort: func(order models.Order) models.TransactionsSort {
|
||||
return func(tx1, tx2 *netmodels.Transaction) bool {
|
||||
return models.Int32Sort(tx1.BlockHeight, tx2.BlockHeight, order)
|
||||
}
|
||||
},
|
||||
display: func(tx *netmodels.Transaction, opts ...color.Option) string {
|
||||
return color.White(opts...)(fmt.Sprintf("%8d", tx.BlockHeight))
|
||||
},
|
||||
}
|
||||
case "ADDRESSES":
|
||||
transactions.columns[i] = transactionsColumn{
|
||||
name: fmt.Sprintf("%10s", columns[i]),
|
||||
width: 10,
|
||||
sort: func(order models.Order) models.TransactionsSort {
|
||||
return func(tx1, tx2 *netmodels.Transaction) bool {
|
||||
return models.IntSort(len(tx1.DestAddresses), len(tx2.DestAddresses), order)
|
||||
}
|
||||
},
|
||||
display: func(tx *netmodels.Transaction, opts ...color.Option) string {
|
||||
return color.White(opts...)(fmt.Sprintf("%10d", len(tx.DestAddresses)))
|
||||
},
|
||||
}
|
||||
case "FEE":
|
||||
transactions.columns[i] = transactionsColumn{
|
||||
name: fmt.Sprintf("%8s", columns[i]),
|
||||
width: 8,
|
||||
sort: func(order models.Order) models.TransactionsSort {
|
||||
return func(tx1, tx2 *netmodels.Transaction) bool {
|
||||
return models.Int64Sort(tx1.TotalFees, tx2.TotalFees, order)
|
||||
}
|
||||
},
|
||||
display: func(tx *netmodels.Transaction, opts ...color.Option) string {
|
||||
return color.White(opts...)(fmt.Sprintf("%8d", tx.TotalFees))
|
||||
},
|
||||
}
|
||||
case "CONFIR":
|
||||
transactions.columns[i] = transactionsColumn{
|
||||
name: fmt.Sprintf("%8s", columns[i]),
|
||||
width: 8,
|
||||
sort: func(order models.Order) models.TransactionsSort {
|
||||
return func(tx1, tx2 *netmodels.Transaction) bool {
|
||||
return models.Int32Sort(tx1.NumConfirmations, tx2.NumConfirmations, order)
|
||||
}
|
||||
},
|
||||
display: func(tx *netmodels.Transaction, opts ...color.Option) string {
|
||||
n := fmt.Sprintf("%8d", tx.NumConfirmations)
|
||||
if tx.NumConfirmations < 6 {
|
||||
return color.Yellow(opts...)(n)
|
||||
}
|
||||
return color.Green(opts...)(n)
|
||||
},
|
||||
}
|
||||
case "TXHASH":
|
||||
transactions.columns[i] = transactionsColumn{
|
||||
name: fmt.Sprintf("%-64s", columns[i]),
|
||||
width: 64,
|
||||
display: func(tx *netmodels.Transaction, opts ...color.Option) string {
|
||||
return color.White(opts...)(fmt.Sprintf("%13s", tx.TxHash))
|
||||
},
|
||||
}
|
||||
case "BLOCKHASH":
|
||||
transactions.columns[i] = transactionsColumn{
|
||||
name: fmt.Sprintf("%-64s", columns[i]),
|
||||
display: func(tx *netmodels.Transaction, opts ...color.Option) string {
|
||||
return color.White(opts...)(fmt.Sprintf("%13s", tx.TxHash))
|
||||
},
|
||||
}
|
||||
case "AMOUNT":
|
||||
transactions.columns[i] = transactionsColumn{
|
||||
name: fmt.Sprintf("%13s", columns[i]),
|
||||
width: 13,
|
||||
sort: func(order models.Order) models.TransactionsSort {
|
||||
return func(tx1, tx2 *netmodels.Transaction) bool {
|
||||
return models.Int64Sort(tx1.Amount, tx2.Amount, order)
|
||||
}
|
||||
},
|
||||
display: func(tx *netmodels.Transaction, opts ...color.Option) string {
|
||||
return color.White(opts...)(printer.Sprintf("%13d", tx.Amount))
|
||||
},
|
||||
}
|
||||
default:
|
||||
transactions.columns[i] = transactionsColumn{
|
||||
name: fmt.Sprintf("%-21s", columns[i]),
|
||||
width: 21,
|
||||
display: func(tx *netmodels.Transaction, opts ...color.Option) string {
|
||||
return "column does not exist"
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return transactions
|
||||
}
|
Loading…
Reference in New Issue