* 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 readme
pull/21/head v0.1.0
Edouard 5 years ago committed by GitHub
parent 314908296a
commit f72c5ca099
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -7,8 +7,8 @@
`lntop` is an interactive text-mode channels viewer for Unix systems.
![lntop-v0.0.0](http://paris.iiens.net/lntop-v0.0.0.png?)
*lntop-v0.0.0*
![lntop-v0.1.0](http://paris.iiens.net/lntop-v0.1.0.png)
*lntop-v0.1.0*
## Install
@ -22,6 +22,9 @@ cd lntop && export GO111MODULE=on && go install -mod=vendor ./...
First time `lntop` is used a config file `.lntop/config.toml` is created
in the user home directory.
Change macaroon path according to your network.
```toml
[logger]
type = "production"
@ -37,8 +40,40 @@ macaroon_timeout = 60
max_msg_recv_size = 52428800
conn_timeout = 1000000
pool_capacity = 3
[views]
# views.channels is the view displaying channel list.
[views.channels]
# It is possible to add, remove and order columns of the
# table with the array columns. The available values are:
columns = [
"STATUS", # status of the channel
"ALIAS", # alias of the channel node
"GAUGE", # ascii bar with percent local/capacity
"LOCAL", # the local amount of the channel
"CAP", # the total capacity of the channel
"SENT", # the total amount sent
"RECEIVED", # the total amount received
"HTLC", # the number of pending HTLC
"UNSETTLED", # the amount unsettled in the channel
"CFEE", # the commit fee
"LAST UPDATE", # last update of the channel
"PRIVATE", # true if channel is private
"ID", # the id of the channel
]
[views.transactions]
# It is possible to add, remove and order columns of the
# table with the array columns. The available values are:
columns = [
"DATE", # date of the transaction
"HEIGHT", # block height of the transaction
"CONFIR", # number of confirmations
"AMOUNT", # amount moved by the transaction
"FEE", # fee of the transaction
"ADDRESSES", # number of transaction output addresses
]
```
Change macaroon path according to your network.
## Docker

@ -15,7 +15,7 @@ import (
"github.com/edouardparis/lntop/ui"
)
const version = "v0.0.3"
const version = "v0.1.0"
// New creates a new cli app.
func New() *cli.App {

@ -14,6 +14,7 @@ import (
type Config struct {
Logger Logger `toml:"logger"`
Network Network `toml:"network"`
Views Views `toml:"views"`
}
type Logger struct {
@ -34,6 +35,15 @@ type Network struct {
PoolCapacity int `toml:"pool_capacity"`
}
type Views struct {
Channels *View `toml:"channels"`
Transactions *View `toml:"transactions"`
}
type View struct {
Columns []string `toml:"columns"`
}
func Load(path string) (*Config, error) {
c := &Config{}

@ -23,6 +23,39 @@ macaroon_timeout = %[8]d
max_msg_recv_size = %[9]d
conn_timeout = %[10]d
pool_capacity = %[11]d
[views]
# views.channels is the view displaying channel list.
[views.channels]
# It is possible to add, remove and order columns of the
# table with the array columns. The available values are:
columns = [
"STATUS", # status of the channel
"ALIAS", # alias of the channel node
"GAUGE", # ascii bar with percent local/capacity
"LOCAL", # the local amount of the channel
"CAP", # the total capacity of the channel
"SENT", # the total amount sent
"RECEIVED", # the total amount received
"HTLC", # the number of pending HTLC
"UNSETTLED", # the amount unsettled in the channel
"CFEE", # the commit fee
"LAST UPDATE", # last update of the channel
"PRIVATE", # true if channel is private
"ID", # the id of the channel
]
[views.transactions]
# It is possible to add, remove and order columns of the
# table with the array columns. The available values are:
columns = [
"DATE", # date of the transaction
"HEIGHT", # block height of the transaction
"CONFIR", # number of confirmations
"AMOUNT", # amount moved by the transaction
"FEE", # fee of the transaction
"ADDRESSES", # number of transaction output addresses
]
`,
cfg.Logger.Type,
cfg.Logger.Dest,

@ -1,14 +1,15 @@
package events
const (
PeerUpdated = "peer.updated"
BlockReceived = "block.received"
InvoiceCreated = "invoice.created"
InvoiceSettled = "invoice.settled"
ChannelPending = "channel.pending"
ChannelActive = "channel.active"
ChannelInactive = "channel.inactive"
ChannelBalanceUpdated = "channel.balance.updated"
ChannelInactive = "channel.inactive"
ChannelPending = "channel.pending"
InvoiceCreated = "invoice.created"
InvoiceSettled = "invoice.settled"
PeerUpdated = "peer.updated"
TransactionCreated = "transaction.created"
WalletBalanceUpdated = "wallet.balance.updated"
)

@ -35,4 +35,8 @@ type Backend interface {
DecodePayReq(context.Context, string) (*models.PayReq, error)
SendPayment(context.Context, *models.PayReq) (*models.Payment, error)
GetTransactions(context.Context) ([]*models.Transaction, error)
SubscribeTransactions(context.Context, chan *models.Transaction) error
}

@ -97,6 +97,38 @@ func (l Backend) SubscribeInvoice(ctx context.Context, channelInvoice chan *mode
}
}
func (l Backend) SubscribeTransactions(ctx context.Context, channel chan *models.Transaction) error {
clt, err := l.Client(ctx)
if err != nil {
return err
}
defer clt.Close()
cltTransactions, err := clt.SubscribeTransactions(ctx, &lnrpc.GetTransactionsRequest{})
if err != nil {
return err
}
for {
select {
case <-ctx.Done():
break
default:
transaction, err := cltTransactions.Recv()
if err != nil {
st, ok := status.FromError(err)
if ok && st.Code() == codes.Canceled {
l.logger.Debug("stopping subscribe transactions: context canceled")
return nil
}
return err
}
channel <- protoToTransaction(transaction)
}
}
}
func (l Backend) SubscribeChannels(ctx context.Context, events chan *models.ChannelUpdate) error {
_, err := l.Client(ctx)
if err != nil {
@ -134,6 +166,23 @@ func (l Backend) NewClientConn() (*grpc.ClientConn, error) {
return newClientConn(l.cfg)
}
func (l Backend) GetTransactions(ctx context.Context) ([]*models.Transaction, error) {
l.logger.Debug("Get transactions...")
clt, err := l.Client(ctx)
if err != nil {
return nil, err
}
defer clt.Close()
req := &lnrpc.GetTransactionsRequest{}
resp, err := clt.GetTransactions(ctx, req)
if err != nil {
return nil, errors.WithStack(err)
}
return protoToTransactions(resp), nil
}
func (l Backend) GetWalletBalance(ctx context.Context) (*models.WalletBalance, error) {
l.logger.Debug("Retrieve wallet balance...")

@ -284,3 +284,28 @@ func protoToRoutingPolicy(resp *lnrpc.RoutingPolicy) *models.RoutingPolicy {
Disabled: resp.Disabled,
}
}
func protoToTransactions(resp *lnrpc.TransactionDetails) []*models.Transaction {
if resp == nil {
return nil
}
transactions := make([]*models.Transaction, len(resp.Transactions))
for i := range resp.Transactions {
transactions[i] = protoToTransaction(resp.Transactions[i])
}
return transactions
}
func protoToTransaction(resp *lnrpc.Transaction) *models.Transaction {
return &models.Transaction{
TxHash: resp.TxHash,
Amount: resp.Amount,
NumConfirmations: resp.NumConfirmations,
BlockHash: resp.BlockHash,
BlockHeight: resp.BlockHeight,
Date: time.Unix(int64(resp.TimeStamp), 0),
TotalFees: resp.TotalFees,
DestAddresses: resp.DestAddresses,
}
}

@ -46,7 +46,11 @@ func (b *Backend) SubscribeChannels(context.Context, chan *models.ChannelUpdate)
return nil
}
func (l *Backend) GetNode(ctx context.Context, pubkey string) (*models.Node, error) {
func (b *Backend) SubscribeTransactions(ctx context.Context, channel chan *models.Transaction) error {
return nil
}
func (b *Backend) GetNode(ctx context.Context, pubkey string) (*models.Node, error) {
return &models.Node{}, nil
}
@ -54,6 +58,10 @@ func (b *Backend) GetWalletBalance(ctx context.Context) (*models.WalletBalance,
return &models.WalletBalance{}, nil
}
func (b *Backend) GetTransactions(ctx context.Context) ([]*models.Transaction, error) {
return []*models.Transaction{}, nil
}
func (b *Backend) GetChannelsBalance(ctx context.Context) (*models.ChannelsBalance, error) {
return &models.ChannelsBalance{}, nil
}

@ -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
}

@ -59,6 +59,35 @@ func (p *PubSub) invoices(ctx context.Context, sub chan *events.Event) {
}()
}
func (p *PubSub) transactions(ctx context.Context, sub chan *events.Event) {
p.wg.Add(3)
transactions := make(chan *models.Transaction)
ctx, cancel := context.WithCancel(ctx)
go func() {
for tx := range transactions {
p.logger.Debug("receive transaction", logging.String("tx_hash", tx.TxHash))
sub <- events.New(events.TransactionCreated)
}
p.wg.Done()
}()
go func() {
err := p.network.SubscribeTransactions(ctx, transactions)
if err != nil {
p.logger.Error("SubscribeTransactions returned an error", logging.Error(err))
}
p.wg.Done()
}()
go func() {
<-p.stop
cancel()
close(transactions)
p.wg.Done()
}()
}
func (p *PubSub) Stop() {
p.stop <- true
close(p.stop)
@ -69,10 +98,12 @@ func (p *PubSub) Run(ctx context.Context, sub chan *events.Event) {
p.logger.Debug("Starting...")
p.invoices(ctx, sub)
p.transactions(ctx, sub)
p.ticker(ctx, sub,
withTickerInfo(),
withTickerChannelsBalance(),
withTickerWalletBalance(),
// no need for ticker Wallet balance, transactions subscriber is enough
// withTickerWalletBalance(),
)
<-p.stop

@ -5,13 +5,104 @@ import "github.com/fatih/color"
type Color color.Color
var (
Yellow = color.New(color.FgYellow).SprintFunc()
Green = color.New(color.FgGreen).SprintFunc()
GreenBg = color.New(color.BgGreen, color.FgBlack).SprintFunc()
Red = color.New(color.FgRed).SprintFunc()
RedBg = color.New(color.BgRed, color.FgBlack).SprintFunc()
Cyan = color.New(color.FgCyan).SprintFunc()
CyanBg = color.New(color.BgCyan, color.FgBlack).SprintFunc()
WhiteBg = color.New(color.BgWhite, color.FgBlack).SprintFunc()
BlackBg = color.New(color.BgBlack, color.FgWhite).SprintFunc()
yellow = color.New(color.FgYellow).SprintFunc()
yellowBold = color.New(color.FgYellow, color.Bold).SprintFunc()
green = color.New(color.FgGreen).SprintFunc()
greenBold = color.New(color.FgGreen, color.Bold).SprintFunc()
greenBg = color.New(color.FgBlack, color.BgGreen).SprintFunc()
magentaBg = color.New(color.FgBlack, color.BgMagenta).SprintFunc()
red = color.New(color.FgRed).SprintFunc()
redBold = color.New(color.FgRed, color.Bold).SprintFunc()
cyan = color.New(color.FgCyan).SprintFunc()
cyanBold = color.New(color.FgCyan, color.Bold).SprintFunc()
cyanBg = color.New(color.BgCyan, color.FgBlack).SprintFunc()
white = color.New().SprintFunc()
whiteBold = color.New(color.Bold).SprintFunc()
blackBg = color.New(color.BgBlack, color.FgWhite).SprintFunc()
black = color.New(color.FgBlack).SprintFunc()
)
type Option func(*options)
type options struct {
bold bool
bg bool
}
func newOptions(opts []Option) options {
options := options{}
for i := range opts {
if opts[i] == nil {
continue
}
opts[i](&options)
}
return options
}
func Bold(o *options) { o.bold = true }
func Background(o *options) { o.bg = true }
func Yellow(opts ...Option) func(a ...interface{}) string {
options := newOptions(opts)
if options.bold {
return yellowBold
}
return yellow
}
func Green(opts ...Option) func(a ...interface{}) string {
options := newOptions(opts)
if options.bold {
return greenBold
}
if options.bg {
return greenBg
}
return green
}
func Red(opts ...Option) func(a ...interface{}) string {
options := newOptions(opts)
if options.bold {
return redBold
}
return red
}
func White(opts ...Option) func(a ...interface{}) string {
options := newOptions(opts)
if options.bold {
return whiteBold
}
return white
}
func Cyan(opts ...Option) func(a ...interface{}) string {
options := newOptions(opts)
if options.bold {
return cyanBold
}
if options.bg {
return cyanBg
}
return cyan
}
func Black(opts ...Option) func(a ...interface{}) string {
options := newOptions(opts)
if options.bg {
return blackBg
}
return black
}
func Magenta(opts ...Option) func(a ...interface{}) string {
options := newOptions(opts)
if options.bg {
return magentaBg
}
return magentaBg
}

@ -8,6 +8,7 @@ import (
"github.com/edouardparis/lntop/app"
"github.com/edouardparis/lntop/events"
"github.com/edouardparis/lntop/logging"
"github.com/edouardparis/lntop/ui/cursor"
"github.com/edouardparis/lntop/ui/models"
"github.com/edouardparis/lntop/ui/views"
)
@ -26,7 +27,7 @@ func (c *controller) layout(g *gocui.Gui) error {
func (c *controller) cursorDown(g *gocui.Gui, v *gocui.View) error {
view := c.views.Get(v)
if view != nil {
return view.CursorDown()
return cursor.Down(view)
}
return nil
}
@ -34,7 +35,7 @@ func (c *controller) cursorDown(g *gocui.Gui, v *gocui.View) error {
func (c *controller) cursorUp(g *gocui.Gui, v *gocui.View) error {
view := c.views.Get(v)
if view != nil {
return view.CursorUp()
return cursor.Up(view)
}
return nil
}
@ -42,7 +43,7 @@ func (c *controller) cursorUp(g *gocui.Gui, v *gocui.View) error {
func (c *controller) cursorRight(g *gocui.Gui, v *gocui.View) error {
view := c.views.Get(v)
if view != nil {
return view.CursorRight()
return cursor.Right(view)
}
return nil
}
@ -50,7 +51,7 @@ func (c *controller) cursorRight(g *gocui.Gui, v *gocui.View) error {
func (c *controller) cursorLeft(g *gocui.Gui, v *gocui.View) error {
view := c.views.Get(v)
if view != nil {
return view.CursorLeft()
return cursor.Left(view)
}
return nil
}
@ -71,6 +72,11 @@ func (c *controller) SetModels(ctx context.Context) error {
return err
}
err = c.models.RefreshTransactions(ctx)
if err != nil {
return err
}
return c.models.RefreshChannels(ctx)
}
@ -89,12 +95,22 @@ func (c *controller) Listen(ctx context.Context, g *gocui.Gui, sub chan *events.
for event := range sub {
c.logger.Debug("event received", logging.String("type", event.Type))
switch event.Type {
case events.TransactionCreated:
refresh(
c.models.RefreshInfo,
c.models.RefreshWalletBalance,
c.models.RefreshTransactions,
)
case events.BlockReceived:
refresh(c.models.RefreshInfo)
refresh(
c.models.RefreshInfo,
c.models.RefreshTransactions,
)
case events.WalletBalanceUpdated:
refresh(
c.models.RefreshInfo,
c.models.RefreshWalletBalance,
c.models.RefreshTransactions,
)
case events.ChannelBalanceUpdated:
refresh(
@ -132,10 +148,6 @@ func (c *controller) Listen(ctx context.Context, g *gocui.Gui, sub chan *events.
}
}
func quit(g *gocui.Gui, v *gocui.View) error {
return gocui.ErrQuit
}
func (c *controller) Help(g *gocui.Gui, v *gocui.View) error {
maxX, maxY := g.Size()
view := c.views.Get(g.CurrentView())
@ -144,117 +156,148 @@ func (c *controller) Help(g *gocui.Gui, v *gocui.View) error {
}
if view.Name() != views.HELP {
c.views.SetPrevious(view)
c.views.Main = view
return c.views.Help.Set(g, 0, -1, maxX, maxY)
}
err := g.DeleteView(views.HELP)
err := view.Delete(g)
if err != nil {
return err
}
if c.views.Previous != nil {
_, err := g.SetCurrentView(c.views.Previous.Name())
if c.views.Main != nil {
_, err := g.SetCurrentView(c.views.Main.Name())
return err
}
return nil
}
func (c *controller) OnEnter(g *gocui.Gui, v *gocui.View) error {
func (c *controller) Menu(g *gocui.Gui, v *gocui.View) error {
maxX, maxY := g.Size()
view := c.views.Get(v)
if view == nil {
if v.Name() == c.views.Help.Name() {
return nil
}
switch view.Name() {
case views.CHANNELS:
c.views.SetPrevious(view)
index := c.views.Channels.Index()
err := c.models.SetCurrentChannel(context.Background(), index)
if err != nil {
return err
}
err = c.views.Channel.Set(g, 0, 6, maxX-1, maxY)
if v.Name() != c.views.Menu.Name() {
err := c.views.Menu.Set(g, 0, 6, 10, maxY)
if err != nil {
return err
}
_, err = g.SetCurrentView(c.views.Channel.Name())
return err
case views.CHANNEL:
err := c.views.Channel.Delete(g)
err = c.views.Main.Set(g, 11, 6, maxX-1, maxY)
if err != nil {
return err
}
if c.views.Previous != nil {
_, err := g.SetCurrentView(c.views.Previous.Name())
return err
}
err = c.views.Channels.Set(g, 0, 6, maxX-1, maxY)
if err != nil {
return err
}
}
return nil
}
func (c *controller) setKeyBinding(g *gocui.Gui) error {
err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit)
if err != nil {
_, err = g.SetCurrentView(c.views.Menu.Name())
return err
}
err = g.SetKeybinding("", gocui.KeyF10, gocui.ModNone, quit)
err := c.views.Menu.Delete(g)
if err != nil {
return err
}
err = g.SetKeybinding("", 'q', gocui.ModNone, quit)
if err != nil {
if c.views.Main != nil {
_, err := g.SetCurrentView(c.views.Main.Name())
return err
}
err = g.SetKeybinding("", gocui.KeyArrowUp, gocui.ModNone, c.cursorUp)
if err != nil {
return err
}
return nil
}
err = g.SetKeybinding("", gocui.KeyArrowDown, gocui.ModNone, c.cursorDown)
if err != nil {
return err
func (c *controller) Order(order models.Order) func(*gocui.Gui, *gocui.View) error {
return func(g *gocui.Gui, v *gocui.View) error {
view := c.views.Get(v)
if view == nil {
return nil
}
switch view.Name() {
case views.CHANNELS:
c.views.Channels.Sort("", order)
case views.TRANSACTIONS:
c.views.Transactions.Sort("", order)
}
return nil
}
}
err = g.SetKeybinding("", gocui.KeyArrowLeft, gocui.ModNone, c.cursorLeft)
if err != nil {
return err
func (c *controller) OnEnter(g *gocui.Gui, v *gocui.View) error {
maxX, maxY := g.Size()
view := c.views.Get(v)
if view == nil {
return nil
}
err = g.SetKeybinding("", gocui.KeyArrowRight, gocui.ModNone, c.cursorRight)
if err != nil {
return err
}
switch view.Name() {
case views.CHANNELS:
index := c.views.Channels.Index()
c.models.Channels.SetCurrent(index)
c.views.Main = c.views.Channel
return ToggleView(g, view, c.views.Channels)
err = g.SetKeybinding("", gocui.KeyEnter, gocui.ModNone, c.OnEnter)
if err != nil {
return err
case views.CHANNEL:
c.views.Main = c.views.Channels
return ToggleView(g, view, c.views.Channels)
case views.MENU:
current := c.views.Menu.Current()
if c.views.Main.Name() == current {
return nil
}
switch current {
case views.TRANSACTIONS:
err := c.views.Main.Delete(g)
if err != nil {
return err
}
c.views.Main = c.views.Transactions
err = c.views.Transactions.Set(g, 11, 6, maxX-1, maxY)
if err != nil {
return err
}
case views.CHANNELS:
err := c.views.Main.Delete(g)
if err != nil {
return err
}
c.views.Main = c.views.Channels
err = c.views.Channels.Set(g, 11, 6, maxX-1, maxY)
if err != nil {
return err
}
}
case views.TRANSACTIONS:
index := c.views.Transactions.Index()
c.models.Transactions.SetCurrent(index)
c.views.Main = c.views.Transaction
return ToggleView(g, view, c.views.Transaction)
case views.TRANSACTION:
c.views.Main = c.views.Transactions
return ToggleView(g, view, c.views.Transactions)
}
return nil
}
err = g.SetKeybinding("", gocui.KeyF1, gocui.ModNone, c.Help)
func ToggleView(g *gocui.Gui, v1, v2 views.View) error {
maxX, maxY := g.Size()
err := v1.Delete(g)
if err != nil {
return err
}
err = g.SetKeybinding("", 'h', gocui.ModNone, c.Help)
err = v2.Set(g, 0, 6, maxX-1, maxY)
if err != nil {
return err
}
return nil
_, err = g.SetCurrentView(v2.Name())
return err
}
func newController(app *app.App) *controller {
@ -262,6 +305,6 @@ func newController(app *app.App) *controller {
return &controller{
logger: app.Logger.With(logging.String("logger", "controller")),
models: m,
views: views.New(m),
views: views.New(app.Config.Views, m),
}
}

@ -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
}

@ -1,15 +1,20 @@
package models
import (
"sort"
"sync"
"github.com/edouardparis/lntop/network/models"
)
type ChannelsSort func(*models.Channel, *models.Channel) bool
type Channels struct {
index map[string]*models.Channel
list []*models.Channel
mu sync.RWMutex
current *models.Channel
index map[string]*models.Channel
list []*models.Channel
sort ChannelsSort
mu sync.RWMutex
}
func (c *Channels) List() []*models.Channel {
@ -20,6 +25,30 @@ func (c *Channels) Len() int {
return len(c.list)
}
func (c *Channels) Swap(i, j int) {
c.list[i], c.list[j] = c.list[j], c.list[i]
}
func (c *Channels) Less(i, j int) bool {
return c.sort(c.list[i], c.list[j])
}
func (c *Channels) Sort(s ChannelsSort) {
if s == nil {
return
}
c.sort = s
sort.Sort(c)
}
func (c *Channels) Current() *models.Channel {
return c.current
}
func (c *Channels) SetCurrent(index int) {
c.current = c.Get(index)
}
func (c *Channels) Get(index int) *models.Channel {
if index < 0 || index > len(c.list)-1 {
return nil
@ -54,6 +83,9 @@ func (c *Channels) Update(newChannel *models.Channel) {
oldChannel, ok := c.index[newChannel.ChannelPoint]
if !ok {
c.Add(newChannel)
if c.sort != nil {
sort.Sort(c)
}
return
}
@ -91,7 +123,3 @@ func NewChannels() *Channels {
index: make(map[string]*models.Channel),
}
}
type Channel struct {
Item *models.Channel
}

@ -15,9 +15,9 @@ type Models struct {
network *network.Network
Info *Info
Channels *Channels
CurrentChannel *Channel
WalletBalance *WalletBalance
ChannelsBalance *ChannelsBalance
Transactions *Transactions
}
func New(app *app.App) *Models {
@ -28,7 +28,7 @@ func New(app *app.App) *Models {
Channels: NewChannels(),
WalletBalance: &WalletBalance{},
ChannelsBalance: &ChannelsBalance{},
CurrentChannel: &Channel{},
Transactions: &Transactions{},
}
}
@ -78,15 +78,6 @@ func (m *Models) RefreshChannels(ctx context.Context) error {
return nil
}
func (m *Models) SetCurrentChannel(ctx context.Context, index int) error {
channel := m.Channels.Get(index)
if channel == nil {
return nil
}
*m.CurrentChannel = Channel{Item: channel}
return nil
}
type WalletBalance struct {
*models.WalletBalance
}

@ -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
}

@ -4,6 +4,7 @@ import (
"context"
"github.com/jroimartin/gocui"
"github.com/pkg/errors"
"github.com/edouardparis/lntop/app"
"github.com/edouardparis/lntop/events"
@ -25,7 +26,7 @@ func Run(ctx context.Context, app *app.App, sub chan *events.Event) error {
g.SetManagerFunc(ctrl.layout)
err = ctrl.setKeyBinding(g)
err = setKeyBinding(ctrl, g)
if err != nil {
return err
}
@ -35,5 +36,5 @@ func Run(ctx context.Context, app *app.App, sub chan *events.Event) error {
err = g.MainLoop()
close(sub)
return err
return errors.WithStack(err)
}

@ -18,8 +18,8 @@ const (
)
type Channel struct {
view *gocui.View
channel *models.Channel
view *gocui.View
channels *models.Channels
}
func (c Channel) Name() string {
@ -27,28 +27,32 @@ func (c Channel) Name() string {
}
func (c Channel) Empty() bool {
return c.channel == nil
return c.channels == nil
}
func (c *Channel) Wrap(v *gocui.View) view {
func (c *Channel) Wrap(v *gocui.View) View {
c.view = v
return c
}
func (c *Channel) CursorDown() error {
return cursorDown(c.view, 1)
func (c Channel) Origin() (int, int) {
return c.view.Origin()
}
func (c *Channel) CursorUp() error {
return cursorUp(c.view, 1)
func (c Channel) Cursor() (int, int) {
return c.view.Cursor()
}
func (c *Channel) CursorRight() error {
return cursorRight(c.view, 1)
func (c Channel) Speed() (int, int, int, int) {
return 1, 1, 1, 1
}
func (c *Channel) CursorLeft() error {
return cursorLeft(c.view, 1)
func (c *Channel) SetCursor(x, y int) error {
return c.view.SetCursor(x, y)
}
func (c *Channel) SetOrigin(x, y int) error {
return c.view.SetOrigin(x, y)
}
func (c *Channel) Set(g *gocui.Gui, x0, y0, x1, y1 int) error {
@ -84,10 +88,12 @@ func (c *Channel) Set(g *gocui.Gui, x0, y0, x1, y1 int) error {
footer.BgColor = gocui.ColorCyan
footer.FgColor = gocui.ColorBlack
footer.Clear()
fmt.Fprintln(footer, fmt.Sprintf("%s%s %s%s %s%s",
color.BlackBg("F1"), "Help",
color.BlackBg("Enter"), "Channels",
color.BlackBg("F10"), "Quit",
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"), "Channels",
blackBg("F10"), "Quit",
))
return nil
}
@ -110,67 +116,70 @@ func (c *Channel) display() {
p := message.NewPrinter(language.English)
v := c.view
v.Clear()
channel := c.channel.Item
fmt.Fprintln(v, color.Green(" [ Channel ]"))
channel := c.channels.Current()
green := color.Green()
cyan := color.Cyan()
red := color.Red()
fmt.Fprintln(v, green(" [ Channel ]"))
fmt.Fprintln(v, fmt.Sprintf("%s %s",
color.Cyan(" Status:"), status(channel)))
cyan(" Status:"), status(channel)))
fmt.Fprintln(v, fmt.Sprintf("%s %d",
color.Cyan(" ID:"), channel.ID))
cyan(" ID:"), channel.ID))
fmt.Fprintln(v, p.Sprintf("%s %d",
color.Cyan(" Capacity:"), channel.Capacity))
cyan(" Capacity:"), channel.Capacity))
fmt.Fprintln(v, p.Sprintf("%s %d",
color.Cyan(" Local Balance:"), channel.LocalBalance))
cyan(" Local Balance:"), channel.LocalBalance))
fmt.Fprintln(v, p.Sprintf("%s %d",
color.Cyan(" Remote Balance:"), channel.RemoteBalance))
cyan(" Remote Balance:"), channel.RemoteBalance))
fmt.Fprintln(v, fmt.Sprintf("%s %s",
color.Cyan(" Channel Point:"), channel.ChannelPoint))
cyan(" Channel Point:"), channel.ChannelPoint))
fmt.Fprintln(v, "")
fmt.Fprintln(v, color.Green(" [ Node ]"))
fmt.Fprintln(v, fmt.Sprintf("%s %s",
color.Cyan(" Alias:"), alias(channel)))
fmt.Fprintln(v, green(" [ Node ]"))
fmt.Fprintln(v, fmt.Sprintf("%s %s",
color.Cyan(" PubKey:"), channel.RemotePubKey))
cyan(" PubKey:"), channel.RemotePubKey))
if channel.Node != nil {
fmt.Fprintln(v, fmt.Sprintf("%s %s",
cyan(" Alias:"), channel.Node.Alias))
fmt.Fprintln(v, p.Sprintf("%s %d",
color.Cyan(" Total Capacity:"), channel.Node.TotalCapacity))
cyan(" Total Capacity:"), channel.Node.TotalCapacity))
fmt.Fprintln(v, p.Sprintf("%s %d",
color.Cyan(" Total Channels:"), channel.Node.NumChannels))
cyan(" Total Channels:"), channel.Node.NumChannels))
}
if channel.Policy1 != nil {
fmt.Fprintln(v, "")
fmt.Fprintln(v, color.Green(" [ Forward Policy Node1 ]"))
fmt.Fprintln(v, green(" [ Forward Policy Node1 ]"))
if channel.Policy1.Disabled {
fmt.Fprintln(v, color.Red("disabled"))
fmt.Fprintln(v, red("disabled"))
}
fmt.Fprintln(v, p.Sprintf("%s %d",
color.Cyan(" Time lock delta:"), channel.Policy1.TimeLockDelta))
cyan(" Time lock delta:"), channel.Policy1.TimeLockDelta))
fmt.Fprintln(v, p.Sprintf("%s %d",
color.Cyan(" Min htlc:"), channel.Policy1.MinHtlc))
cyan(" Min htlc:"), channel.Policy1.MinHtlc))
fmt.Fprintln(v, p.Sprintf("%s %d",
color.Cyan(" Fee base msat:"), channel.Policy1.FeeBaseMsat))
cyan(" Fee base msat:"), channel.Policy1.FeeBaseMsat))
fmt.Fprintln(v, p.Sprintf("%s %d",
color.Cyan("Fee rate milli msat:"), channel.Policy1.FeeRateMilliMsat))
cyan("Fee rate milli msat:"), channel.Policy1.FeeRateMilliMsat))
}
if channel.Policy2 != nil {
fmt.Fprintln(v, "")
fmt.Fprintln(v, color.Green(" [ Forward Policy Node 2 ]"))
fmt.Fprintln(v, green(" [ Forward Policy Node 2 ]"))
if channel.Policy2.Disabled {
fmt.Fprintln(v, color.Red("disabled"))
fmt.Fprintln(v, red("disabled"))
}
fmt.Fprintln(v, p.Sprintf("%s %d",
color.Cyan(" Time lock delta:"), channel.Policy2.TimeLockDelta))
cyan(" Time lock delta:"), channel.Policy2.TimeLockDelta))
fmt.Fprintln(v, p.Sprintf("%s %d",
color.Cyan(" Min htlc:"), channel.Policy2.MinHtlc))
cyan(" Min htlc:"), channel.Policy2.MinHtlc))
fmt.Fprintln(v, p.Sprintf("%s %d",
color.Cyan(" Fee base msat:"), channel.Policy2.FeeBaseMsat))
cyan(" Fee base msat:"), channel.Policy2.FeeBaseMsat))
fmt.Fprintln(v, p.Sprintf("%s %d",
color.Cyan("Fee rate milli msat:"), channel.Policy2.FeeRateMilliMsat))
cyan("Fee rate milli msat:"), channel.Policy2.FeeRateMilliMsat))
}
}
func NewChannel(channel *models.Channel) *Channel {
return &Channel{channel: channel}
func NewChannel(channels *models.Channels) *Channel {
return &Channel{channels: channels}
}

@ -8,6 +8,7 @@ import (
"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"
@ -19,81 +20,190 @@ const (
CHANNELS_FOOTER = "channels_footer"
)
var DefaultChannelsColumns = []string{
"STATUS",
"ALIAS",
"GAUGE",
"LOCAL",
"CAP",
"SENT",
"RECEIVED",
"HTLC",
"UNSETTLED",
"CFEE",
"LAST UPDATE",
"PRIVATE",
"ID",
}
type Channels struct {
columns *gocui.View
view *gocui.View
channels *models.Channels
cfg *config.View
columns []channelsColumn
columnsView *gocui.View
view *gocui.View
channels *models.Channels
ox, oy int
cx, cy int
}
func (c Channels) Index() int {
_, oy := c.view.Origin()
_, cy := c.view.Cursor()
return cy + oy
type channelsColumn struct {
name string
width int
sorted bool
sort func(models.Order) models.ChannelsSort
display func(*netmodels.Channel, ...color.Option) string
}
func (c Channels) Name() string {
return CHANNELS
}
func (c *Channels) Wrap(v *gocui.View) view {
func (c *Channels) Wrap(v *gocui.View) View {
c.view = v
return c
}
func (c *Channels) CursorDown() error {
return cursorDown(c.view, 1)
func (c Channels) 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 *Channels) CursorUp() error {
return cursorUp(c.view, 1)
func (c Channels) Sort(column string, order models.Order) {
if column == "" {
index := c.currentColumnIndex()
col := c.columns[index]
if col.sort == nil {
return
}
c.channels.Sort(col.sort(order))
for i := range c.columns {
c.columns[i].sorted = (i == index)
}
}
}
func (c Channels) Origin() (int, int) {
return c.ox, c.oy
}
func (c Channels) Cursor() (int, int) {
return c.cx, c.cy
}
func (c *Channels) CursorRight() error {
err := cursorRight(c.columns, 2)
func (c *Channels) 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
}
return cursorRight(c.view, 2)
c.cx, c.cy = cx, cy
return nil
}
func (c *Channels) 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 *Channels) 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 *Channels) CursorLeft() error {
err := cursorLeft(c.columns, 2)
func (c Channels) Index() int {
_, oy := c.Origin()
_, cy := c.Cursor()
return cy + oy
}
func (c Channels) Delete(g *gocui.Gui) error {
err := g.DeleteView(CHANNELS_COLUMNS)
if err != nil {
return err
}
err = g.DeleteView(CHANNELS)
if err != nil {
return err
}
return cursorLeft(c.view, 2)
return g.DeleteView(CHANNELS_FOOTER)
}
func (c *Channels) Set(g *gocui.Gui, x0, y0, x1, y1 int) error {
var err error
c.columns, err = g.SetView(CHANNELS_COLUMNS, x0-1, y0, x1+2, y0+2)
setCursor := false
c.columnsView, err = g.SetView(CHANNELS_COLUMNS, x0-1, y0, x1+2, y0+2)
if err != nil {
if err != gocui.ErrUnknownView {
return err
}
setCursor = true
}
c.columns.Frame = false
c.columns.BgColor = gocui.ColorGreen
c.columns.FgColor = gocui.ColorBlack
displayChannelsColumns(c.columns)
c.columnsView.Frame = false
c.columnsView.BgColor = gocui.ColorGreen
c.columnsView.FgColor = gocui.ColorBlack
c.view, err = g.SetView(CHANNELS, x0-1, y0+1, x1+2, y1-1)
if err != nil {
if err != gocui.ErrUnknownView {
return err
}
_, err = g.SetCurrentView(CHANNELS)
if err != nil {
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()
@ -107,120 +217,278 @@ func (c *Channels) Set(g *gocui.Gui, x0, y0, x1, y1 int) error {
footer.BgColor = gocui.ColorCyan
footer.FgColor = gocui.ColorBlack
footer.Clear()
fmt.Fprintln(footer, fmt.Sprintf("%s%s %s%s %s%s",
color.BlackBg("F1"), "Help",
color.BlackBg("Enter"), "Channel",
color.BlackBg("F10"), "Quit",
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"), "Channel",
blackBg("F10"), "Quit",
))
return nil
}
func displayChannelsColumns(v *gocui.View) {
v.Clear()
fmt.Fprintln(v, fmt.Sprintf("%-13s %-25s %-21s %12s %12s %5s %-10s %-6s %-15s %s %-19s",
"STATUS",
"ALIAS",
"GAUGE",
"LOCAL",
"CAP",
"HTLC",
"UNSETTLED",
"CFEE",
"Last Update",
"PRIVATE",
"ID",
))
}
func (c *Channels) display() {
p := message.NewPrinter(language.English)
c.view.Clear()
for _, item := range c.channels.List() {
line := fmt.Sprintf("%s %-25s %s %s %s %5d %s %s %s %s %19s %500s",
status(item),
alias(item),
gauge(item),
color.Cyan(p.Sprintf("%12d", item.LocalBalance)),
p.Sprintf("%12d", item.Capacity),
len(item.PendingHTLC),
color.Yellow(p.Sprintf("%10d", item.UnsettledBalance)),
p.Sprintf("%6d", item.CommitFee),
lastUpdate(item),
channelPrivate(item),
channelID(item),
"",
)
fmt.Fprintln(c.view, line)
c.columnsView.Clear()
var buffer bytes.Buffer
currentColumnIndex := c.currentColumnIndex()
for i := range c.columns {
if currentColumnIndex == 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())
func channelPrivate(c *netmodels.Channel) string {
if c.Private {
return color.Red("private")
c.view.Clear()
for _, item := range c.channels.List() {
var buffer bytes.Buffer
for i := range c.columns {
var opt color.Option
if currentColumnIndex == i {
opt = color.Bold
}
buffer.WriteString(c.columns[i].display(item, opt))
buffer.WriteString(" ")
}
fmt.Fprintln(c.view, buffer.String())
}
return color.Green("public ")
}
func channelID(c *netmodels.Channel) string {
if c.ID == 0 {
return ""
func NewChannels(cfg *config.View, chans *models.Channels) *Channels {
channels := &Channels{
cfg: cfg,
channels: chans,
}
return fmt.Sprintf("%d", c.ID)
}
printer := message.NewPrinter(language.English)
func alias(c *netmodels.Channel) string {
if c.Node == nil || c.Node.Alias == "" {
return c.RemotePubKey[:24]
} else if len(c.Node.Alias) > 25 {
return c.Node.Alias[:24]
columns := DefaultChannelsColumns
if cfg != nil && len(cfg.Columns) != 0 {
columns = cfg.Columns
}
return c.Node.Alias
}
channels.columns = make([]channelsColumn, len(columns))
func lastUpdate(c *netmodels.Channel) string {
if c.LastUpdate != nil {
return color.Cyan(
fmt.Sprintf("%15s", c.LastUpdate.Format("15:04:05 Jan _2")),
)
for i := range columns {
switch columns[i] {
case "STATUS":
channels.columns[i] = channelsColumn{
width: 13,
name: fmt.Sprintf("%-13s", columns[i]),
display: status,
}
case "ALIAS":
channels.columns[i] = channelsColumn{
width: 25,
name: fmt.Sprintf("%-25s", columns[i]),
display: func(c *netmodels.Channel, opts ...color.Option) string {
var alias string
if c.Node == nil || c.Node.Alias == "" {
alias = c.RemotePubKey[:24]
} else if len(c.Node.Alias) > 25 {
alias = c.Node.Alias[:24]
} else {
alias = c.Node.Alias
}
return color.White(opts...)(fmt.Sprintf("%-25s", alias))
},
}
case "GAUGE":
channels.columns[i] = channelsColumn{
width: 21,
name: fmt.Sprintf("%-21s", columns[i]),
sort: func(order models.Order) models.ChannelsSort {
return func(c1, c2 *netmodels.Channel) bool {
return models.Int64Sort(
c1.LocalBalance*100/c1.Capacity,
c2.LocalBalance*100/c2.Capacity,
order)
}
},
display: func(c *netmodels.Channel, opts ...color.Option) string {
index := int(c.LocalBalance * int64(15) / c.Capacity)
var buffer bytes.Buffer
cyan := color.Cyan(opts...)
white := color.White(opts...)
for i := 0; i < 15; i++ {
if i < index {
buffer.WriteString(cyan("|"))
continue
}
buffer.WriteString(" ")
}
return fmt.Sprintf("%s%s%s",
white("["),
buffer.String(),
white(fmt.Sprintf("] %2d%%", c.LocalBalance*100/c.Capacity)))
},
}
case "LOCAL":
channels.columns[i] = channelsColumn{
width: 12,
name: fmt.Sprintf("%12s", columns[i]),
sort: func(order models.Order) models.ChannelsSort {
return func(c1, c2 *netmodels.Channel) bool {
return models.Int64Sort(c1.LocalBalance, c2.LocalBalance, order)
}
},
display: func(c *netmodels.Channel, opts ...color.Option) string {
return color.Cyan(opts...)(printer.Sprintf("%12d", c.LocalBalance))
},
}
case "CAP":
channels.columns[i] = channelsColumn{
width: 12,
name: fmt.Sprintf("%12s", columns[i]),
sort: func(order models.Order) models.ChannelsSort {
return func(c1, c2 *netmodels.Channel) bool {
return models.Int64Sort(c1.Capacity, c2.Capacity, order)
}
},
display: func(c *netmodels.Channel, opts ...color.Option) string {
return color.White(opts...)(printer.Sprintf("%12d", c.Capacity))
},
}
case "SENT":
channels.columns[i] = channelsColumn{
width: 12,
name: fmt.Sprintf("%12s", columns[i]),
sort: func(order models.Order) models.ChannelsSort {
return func(c1, c2 *netmodels.Channel) bool {
return models.Int64Sort(c1.TotalAmountSent, c2.TotalAmountSent, order)
}
},
display: func(c *netmodels.Channel, opts ...color.Option) string {
return color.Cyan(opts...)(printer.Sprintf("%12d", c.TotalAmountSent))
},
}
case "RECEIVED":
channels.columns[i] = channelsColumn{
width: 12,
name: fmt.Sprintf("%12s", columns[i]),
sort: func(order models.Order) models.ChannelsSort {
return func(c1, c2 *netmodels.Channel) bool {
return models.Int64Sort(c1.TotalAmountReceived, c2.TotalAmountReceived, order)
}
},
display: func(c *netmodels.Channel, opts ...color.Option) string {
return color.Cyan(opts...)(printer.Sprintf("%12d", c.TotalAmountReceived))
},
}
case "HTLC":
channels.columns[i] = channelsColumn{
width: 5,
name: fmt.Sprintf("%5s", columns[i]),
sort: func(order models.Order) models.ChannelsSort {
return func(c1, c2 *netmodels.Channel) bool {
return models.IntSort(len(c1.PendingHTLC), len(c2.PendingHTLC), order)
}
},
display: func(c *netmodels.Channel, opts ...color.Option) string {
return color.Yellow(opts...)(fmt.Sprintf("%5d", len(c.PendingHTLC)))
},
}
case "UNSETTLED":
channels.columns[i] = channelsColumn{
width: 10,
name: fmt.Sprintf("%-10s", columns[i]),
sort: func(order models.Order) models.ChannelsSort {
return func(c1, c2 *netmodels.Channel) bool {
return models.Int64Sort(c1.UnsettledBalance, c2.UnsettledBalance, order)
}
},
display: func(c *netmodels.Channel, opts ...color.Option) string {
return color.Yellow(opts...)(printer.Sprintf("%10d", c.UnsettledBalance))
},
}
case "CFEE":
channels.columns[i] = channelsColumn{
width: 6,
name: fmt.Sprintf("%-6s", columns[i]),
sort: func(order models.Order) models.ChannelsSort {
return func(c1, c2 *netmodels.Channel) bool {
return models.Int64Sort(c1.CommitFee, c2.CommitFee, order)
}
},
display: func(c *netmodels.Channel, opts ...color.Option) string {
return color.White(opts...)(printer.Sprintf("%6d", c.CommitFee))
},
}
case "LAST UPDATE":
channels.columns[i] = channelsColumn{
width: 15,
name: fmt.Sprintf("%-15s", columns[i]),
sort: func(order models.Order) models.ChannelsSort {
return func(c1, c2 *netmodels.Channel) bool {
return models.DateSort(c1.LastUpdate, c2.LastUpdate, order)
}
},
display: func(c *netmodels.Channel, opts ...color.Option) string {
if c.LastUpdate != nil {
return color.Cyan(opts...)(
fmt.Sprintf("%15s", c.LastUpdate.Format("15:04:05 Jan _2")),
)
}
return fmt.Sprintf("%15s", "")
},
}
case "PRIVATE":
channels.columns[i] = channelsColumn{
width: 7,
name: fmt.Sprintf("%-7s", columns[i]),
display: func(c *netmodels.Channel, opts ...color.Option) string {
if c.Private {
return color.Red(opts...)("private")
}
return color.Green(opts...)("public ")
},
}
case "ID":
channels.columns[i] = channelsColumn{
width: 19,
name: fmt.Sprintf("%-19s", columns[i]),
display: func(c *netmodels.Channel, opts ...color.Option) string {
if c.ID == 0 {
return fmt.Sprintf("%-19s", "")
}
return color.White(opts...)(fmt.Sprintf("%d", c.ID))
},
}
default:
channels.columns[i] = channelsColumn{
width: 21,
name: fmt.Sprintf("%-21s", columns[i]),
display: func(c *netmodels.Channel, opts ...color.Option) string {
return "column does not exist"
},
}
}
}
return fmt.Sprintf("%15s", "")
return channels
}
func status(c *netmodels.Channel) string {
func status(c *netmodels.Channel, opts ...color.Option) string {
switch c.Status {
case netmodels.ChannelActive:
return color.Green(fmt.Sprintf("%-13s", "active"))
return color.Green(opts...)(fmt.Sprintf("%-13s", "active"))
case netmodels.ChannelInactive:
return color.Red(fmt.Sprintf("%-13s", "inactive"))
return color.Red(opts...)(fmt.Sprintf("%-13s", "inactive"))
case netmodels.ChannelOpening:
return color.Yellow(fmt.Sprintf("%-13s", "opening"))
return color.Yellow(opts...)(fmt.Sprintf("%-13s", "opening"))
case netmodels.ChannelClosing:
return color.Yellow(fmt.Sprintf("%-13s", "closing"))
return color.Yellow(opts...)(fmt.Sprintf("%-13s", "closing"))
case netmodels.ChannelForceClosing:
return color.Yellow(fmt.Sprintf("%-13s", "force closing"))
return color.Yellow(opts...)(fmt.Sprintf("%-13s", "force closing"))
case netmodels.ChannelWaitingClose:
return color.Yellow(fmt.Sprintf("%-13s", "waiting close"))
return color.Yellow(opts...)(fmt.Sprintf("%-13s", "waiting close"))
}
return ""
}
func gauge(c *netmodels.Channel) string {
index := int(c.LocalBalance * int64(15) / c.Capacity)
var buffer bytes.Buffer
for i := 0; i < 15; i++ {
if i < index {
buffer.WriteString(color.Cyan("|"))
continue
}
buffer.WriteString(" ")
}
return fmt.Sprintf("[%s] %2d%%", buffer.String(), c.LocalBalance*100/c.Capacity)
}
func NewChannels(channels *models.Channels) *Channels {
return &Channels{channels: channels}
}

@ -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
}

@ -44,19 +44,20 @@ func (h *Header) Set(g *gocui.Gui, x0, y0, x1, y1 int) error {
network = "mainnet"
}
sync := color.Yellow("[syncing]")
sync := color.Yellow()("[syncing]")
if h.Info.Synced {
sync = color.Green("[synced]")
sync = color.Green()("[synced]")
}
v.Clear()
cyan := color.Cyan()
fmt.Fprintln(v, fmt.Sprintf("%s %s %s %s %s %s",
color.CyanBg(h.Info.Alias),
color.Cyan(fmt.Sprintf("%s-v%s", "lnd", version)),
color.Cyan(color.Background)(h.Info.Alias),
cyan(fmt.Sprintf("%s-v%s", "lnd", version)),
fmt.Sprintf("%s %s", chain, network),
sync,
fmt.Sprintf("%s %d", color.Cyan("height:"), h.Info.BlockHeight),
fmt.Sprintf("%s %d", color.Cyan("peers:"), h.Info.NumPeers),
fmt.Sprintf("%s %d", cyan("height:"), h.Info.BlockHeight),
fmt.Sprintf("%s %d", cyan("peers:"), h.Info.NumPeers),
))
return nil
}

@ -9,7 +9,7 @@ import (
)
const (
version = "v0.0.3"
version = "v0.1.0"
HELP = "help"
)
@ -21,25 +21,33 @@ func (h Help) Name() string {
return HELP
}
func (h *Help) Wrap(v *gocui.View) view {
func (h *Help) Wrap(v *gocui.View) View {
h.view = v
return h
}
func (h *Help) CursorDown() error {
return cursorDown(h.view, 1)
func (h Help) Delete(g *gocui.Gui) error {
return g.DeleteView(HELP)
}
func (h *Help) CursorUp() error {
return cursorUp(h.view, 1)
func (h Help) Origin() (int, int) {
return h.view.Origin()
}
func (h *Help) CursorRight() error {
return cursorRight(h.view, 1)
func (h Help) Cursor() (int, int) {
return h.view.Cursor()
}
func (h *Help) CursorLeft() error {
return cursorLeft(h.view, 1)
func (h Help) Speed() (int, int, int, int) {
return 1, 1, 1, 1
}
func (h *Help) SetCursor(x, y int) error {
return h.view.SetCursor(x, y)
}
func (h *Help) SetOrigin(x, y int) error {
return h.view.SetOrigin(x, y)
}
func (h Help) Set(g *gocui.Gui, x0, y0, x1, y1 int) error {
@ -51,13 +59,20 @@ func (h Help) Set(g *gocui.Gui, x0, y0, x1, y1 int) error {
}
}
h.view.Frame = false
cyan := color.Cyan()
fmt.Fprintln(h.view, fmt.Sprintf("lntop %s - (C) 2019 Edouard Paris", version))
fmt.Fprintln(h.view, "Released under the MIT License")
fmt.Fprintln(h.view, "")
fmt.Fprintln(h.view, fmt.Sprintf("%6s %s",
color.Cyan("F1 h:"), "show/close this help screen"))
cyan("F1 h:"), "show/close this help screen"))
fmt.Fprintln(h.view, fmt.Sprintf("%6s %s",
cyan("F2 m:"), "show/close the menu sidebar"))
fmt.Fprintln(h.view, fmt.Sprintf("%6s %s",
cyan("F10 q:"), "quit"))
fmt.Fprintln(h.view, "")
fmt.Fprintln(h.view, fmt.Sprintf("%6s %s",
color.Cyan("F10 q:"), "quit"))
cyan(" a d:"), "apply asc/desc order to the rows according to the selected column value"))
_, err = g.SetCurrentView(HELP)
return err
}

@ -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{} }

@ -53,31 +53,35 @@ func (s *Summary) Set(g *gocui.Gui, x0, y0, x1, y1 int) error {
func (s *Summary) display() {
s.left.Clear()
p := message.NewPrinter(language.English)
fmt.Fprintln(s.left, color.Green("[ Channels ]"))
green := color.Green()
yellow := color.Yellow()
cyan := color.Cyan()
red := color.Red()
fmt.Fprintln(s.left, green("[ Channels ]"))
fmt.Fprintln(s.left, p.Sprintf("%s %d (%s|%s)",
color.Cyan("balance:"),
cyan("balance:"),
s.channelsBalance.Balance+s.channelsBalance.PendingOpenBalance,
color.Green(p.Sprintf("%d", s.channelsBalance.Balance)),
color.Yellow(p.Sprintf("%d", s.channelsBalance.PendingOpenBalance)),
green(p.Sprintf("%d", s.channelsBalance.Balance)),
yellow(p.Sprintf("%d", s.channelsBalance.PendingOpenBalance)),
))
fmt.Fprintln(s.left, fmt.Sprintf("%s %d %s %d %s %d %s",
color.Cyan("state :"),
s.info.NumActiveChannels, color.Green("active"),
s.info.NumPendingChannels, color.Yellow("pending"),
s.info.NumInactiveChannels, color.Red("inactive"),
cyan("state :"),
s.info.NumActiveChannels, green("active"),
s.info.NumPendingChannels, yellow("pending"),
s.info.NumInactiveChannels, red("inactive"),
))
fmt.Fprintln(s.left, fmt.Sprintf("%s %s",
color.Cyan("gauge :"),
cyan("gauge :"),
gaugeTotal(s.channelsBalance.Balance, s.channels.List()),
))
s.right.Clear()
fmt.Fprintln(s.right, color.Green("[ Wallet ]"))
fmt.Fprintln(s.right, green("[ Wallet ]"))
fmt.Fprintln(s.right, p.Sprintf("%s %d (%s|%s)",
color.Cyan("balance:"),
cyan("balance:"),
s.walletBalance.TotalBalance,
color.Green(p.Sprintf("%d", s.walletBalance.ConfirmedBalance)),
color.Yellow(p.Sprintf("%d", s.walletBalance.UnconfirmedBalance)),
green(p.Sprintf("%d", s.walletBalance.ConfirmedBalance)),
yellow(p.Sprintf("%d", s.walletBalance.UnconfirmedBalance)),
))
}
@ -93,9 +97,10 @@ func gaugeTotal(balance int64, channels []*netmodels.Channel) string {
index := int(balance * int64(20) / capacity)
var buffer bytes.Buffer
cyan := color.Cyan()
for i := 0; i < 20; i++ {
if i < index {
buffer.WriteString(color.Cyan("|"))
buffer.WriteString(cyan("|"))
continue
}
buffer.WriteString(" ")

@ -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
}

@ -1,30 +1,36 @@
package views
import (
"github.com/edouardparis/lntop/ui/models"
"github.com/jroimartin/gocui"
"github.com/pkg/errors"
"github.com/edouardparis/lntop/config"
"github.com/edouardparis/lntop/ui/cursor"
"github.com/edouardparis/lntop/ui/models"
)
type view interface {
type View interface {
Set(*gocui.Gui, int, int, int, int) error
Wrap(*gocui.View) view
CursorLeft() error
CursorRight() error
CursorUp() error
CursorDown() error
Delete(*gocui.Gui) error
Wrap(*gocui.View) View
Name() string
cursor.View
}
type Views struct {
Previous view
Help *Help
Header *Header
Summary *Summary
Channels *Channels
Channel *Channel
Main View
Help *Help
Header *Header
Menu *Menu
Summary *Summary
Channels *Channels
Channel *Channel
Transactions *Transactions
Transaction *Transaction
}
func (v Views) Get(vi *gocui.View) view {
func (v Views) Get(vi *gocui.View) View {
if vi == nil {
return nil
}
@ -33,17 +39,19 @@ func (v Views) Get(vi *gocui.View) view {
return v.Channels.Wrap(vi)
case HELP:
return v.Help.Wrap(vi)
case MENU:
return v.Menu.Wrap(vi)
case CHANNEL:
return v.Channel.Wrap(vi)
case TRANSACTIONS:
return v.Transactions.Wrap(vi)
case TRANSACTION:
return v.Transaction.Wrap(vi)
default:
return nil
}
}
func (v *Views) SetPrevious(p view) {
v.Previous = p
}
func (v *Views) Layout(g *gocui.Gui, maxX, maxY int) error {
err := v.Header.Set(g, 0, -1, maxX, 1)
if err != nil {
@ -55,15 +63,49 @@ func (v *Views) Layout(g *gocui.Gui, maxX, maxY int) error {
return err
}
return v.Channels.Set(g, 0, 6, maxX-1, maxY)
current := g.CurrentView()
if current != nil {
switch current.Name() {
case v.Help.Name():
return nil
case v.Menu.Name():
err = v.Menu.Set(g, 0, 6, 10, maxY)
if err != nil {
return err
}
err = v.Main.Set(g, 11, 6, maxX-1, maxY)
if err != nil {
return err
}
return nil
}
}
err = v.Main.Set(g, 0, 6, maxX-1, maxY)
if err != nil && err != gocui.ErrUnknownView {
return err
}
_, err = g.SetCurrentView(v.Main.Name())
if err != nil {
return errors.WithStack(err)
}
return nil
}
func New(m *models.Models) *Views {
func New(cfg config.Views, m *models.Models) *Views {
main := NewChannels(cfg.Channels, m.Channels)
return &Views{
Header: NewHeader(m.Info),
Help: NewHelp(),
Summary: NewSummary(m.Info, m.ChannelsBalance, m.WalletBalance, m.Channels),
Channels: NewChannels(m.Channels),
Channel: NewChannel(m.CurrentChannel),
Header: NewHeader(m.Info),
Help: NewHelp(),
Menu: NewMenu(),
Summary: NewSummary(m.Info, m.ChannelsBalance, m.WalletBalance, m.Channels),
Channels: main,
Channel: NewChannel(m.Channels),
Transactions: NewTransactions(cfg.Transactions, m.Transactions),
Transaction: NewTransaction(m.Transactions),
Main: main,
}
}

Loading…
Cancel
Save