diff --git a/internal/lnd/channel.go b/internal/lnd/channel.go index 190e21f..ae4a93c 100644 --- a/internal/lnd/channel.go +++ b/internal/lnd/channel.go @@ -8,7 +8,7 @@ import ( // A wrapper around lndclient's ChannelInfo combined with a node Alias type Channel struct { - Info lndclient.ChannelInfo + Info lndclient.ChannelInfo Alias string } @@ -19,6 +19,10 @@ func (c Channel) FilterValue() string { // bubbletea interface function func (c Channel) Title() string { + if !c.Info.Active { + return c.Alias + " (OFFLINE)" + } + return c.Alias } @@ -28,8 +32,8 @@ func (c Channel) Description() string { localBalance := c.Info.LocalBalance.ToBTC() localBalancePercentage := localBalance / c.Info.Capacity.ToBTC() prog := progress.New(progress.WithoutPercentage()) - - return satsToShortString(c.Info.LocalBalance.ToUnit(btcutil.AmountSatoshi)) + - " " + prog.ViewAs(localBalancePercentage) + " " + - satsToShortString(c.Info.RemoteBalance.ToUnit(btcutil.AmountSatoshi)) -} \ No newline at end of file + + return satsToShortString(c.Info.LocalBalance.ToUnit(btcutil.AmountSatoshi)) + + " " + prog.ViewAs(localBalancePercentage) + " " + + satsToShortString(c.Info.RemoteBalance.ToUnit(btcutil.AmountSatoshi)) +} diff --git a/internal/tui/base.go b/internal/tui/base.go new file mode 100644 index 0000000..4525ae0 --- /dev/null +++ b/internal/tui/base.go @@ -0,0 +1,33 @@ +package tui + +import ( + "github.com/charmbracelet/bubbles/key" + tea "github.com/charmbracelet/bubbletea" +) + +// Base model that handles logic common to all views + +type BaseModel struct{} + +func NewBaseModel() *BaseModel { + return &BaseModel{} +} + +func (m BaseModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyMsg: + switch { + case key.Matches(msg, Keymap.Quit): + return m, tea.Quit + } + } + return m, nil +} + +func (m BaseModel) Init() tea.Cmd { + return nil +} + +func (m BaseModel) View() string { + return "" +} diff --git a/internal/tui/channel.go b/internal/tui/channel.go new file mode 100644 index 0000000..8856960 --- /dev/null +++ b/internal/tui/channel.go @@ -0,0 +1,82 @@ +package tui + +import ( + "context" + "fmt" + + "github.com/ardevd/flash/internal/lnd" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/lightninglabs/lndclient" +) + +// Model for the Channel view +type ChannelModel struct { + styles *Styles + channel lnd.Channel + lndService *lndclient.GrpcLndServices + ctx context.Context + dashboard *DashboardModel + base BaseModel +} + +func NewChannelModel(service *lndclient.GrpcLndServices, channel lnd.Channel, dashboard *DashboardModel) *ChannelModel { + m := ChannelModel{lndService: service, ctx: context.Background(), channel: channel, base: *NewBaseModel(), dashboard: dashboard} + m.styles = GetDefaultStyles() + return &m +} + +func (m *ChannelModel) initData(width, height int) { + // TODO: Init list data + +} + +func (m ChannelModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + // Base model logic + model, cmd := m.base.Update(msg) + if cmd != nil { + return model, cmd + } + + switch msg := msg.(type) { + case tea.WindowSizeMsg: + windowSizeMsg = msg + + v, h := m.styles.BorderedStyle.GetFrameSize() + m.initData(windowSizeMsg.Width-h, windowSizeMsg.Height-v) + + } + return m, cmd +} + +func (m ChannelModel) Init() tea.Cmd { + return nil +} + +func (m ChannelModel) getChannelStateView() string { + active := m.channel.Info.Active + var stateText string + if active { + stateText = m.styles.PositiveString("ONLINE") + "\n" + m.channel.Info.Uptime.String() + } else { + stateText = m.styles.NegativeString("OFFLINE\n") + } + + return stateText + "\n" + m.styles.SubKeyword("Pending HTLCs: ") + fmt.Sprintf("%d", m.channel.Info.NumPendingHtlcs) + +} + +func (m ChannelModel) View() string { + s := m.styles + channelInfoView := lipgloss.JoinVertical(lipgloss.Left, s.BorderedStyle.Render( + s.Keyword(m.channel.Alias)+ + "\n"+s.SubKeyword("pubkey:")+m.channel.Info.PubKeyBytes.String()+ + "\n"+s.SubKeyword("chanpoint: ")+m.channel.Info.ChannelPoint)) + + channelStateView := lipgloss.JoinVertical(lipgloss.Center, s.BorderedStyle.Render(m.getChannelStateView())) + + topView := lipgloss.JoinHorizontal(lipgloss.Left, channelInfoView, channelStateView) + + return lipgloss.JoinVertical(lipgloss.Left, + topView) +} diff --git a/internal/tui/dashboard.go b/internal/tui/dashboard.go index 251d992..d2192a7 100644 --- a/internal/tui/dashboard.go +++ b/internal/tui/dashboard.go @@ -28,6 +28,7 @@ var formSelection string func InitDashboard(service *lndclient.GrpcLndServices, nodeData lnd.NodeData) *DashboardModel { m := DashboardModel{lndService: service, ctx: context.Background(), nodeData: nodeData} m.styles = GetDefaultStyles() + m.base = *NewBaseModel() return &m } @@ -46,6 +47,12 @@ func (m *DashboardModel) initData(width, height int) { } func (m DashboardModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + // Base model logic + model, cmd := m.base.Update(msg) + if cmd != nil { + return model, cmd + } + switch msg := msg.(type) { case tea.WindowSizeMsg: windowSizeMsg = msg @@ -64,14 +71,12 @@ func (m DashboardModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch m.focused { case paymentTools, messageTools, channelTools: return m.handleFormClick() + case channels: + return m.handleChannelClick() } - case key.Matches(msg, Keymap.Quit): - m.quitting = true - return m, tea.Quit } } - var cmd tea.Cmd switch m.focused { case payments: m.lists[m.focused], cmd = m.lists[m.focused].Update(msg) @@ -97,10 +102,6 @@ func (m DashboardModel) Init() tea.Cmd { func (m DashboardModel) View() string { s := m.styles - if m.quitting { - return "" - } - if m.loaded { channelsView := m.lists[channels].View() paymentsView := m.lists[payments].View() @@ -223,6 +224,11 @@ func (m *DashboardModel) generateMessageToolsForm() *huh.Form { return huh.NewForm(huh.NewGroup(s)) } +func (m *DashboardModel) handleChannelClick() (tea.Model, tea.Cmd) { + selectedChannel := m.lists[m.focused].SelectedItem().(lnd.Channel) + return NewChannelModel(m.lndService, selectedChannel, m).Update(windowSizeMsg) +} + func (m *DashboardModel) handleFormClick() (tea.Model, tea.Cmd) { i := m.getInvoiceModel() return i.Update(windowSizeMsg) @@ -274,5 +280,5 @@ type DashboardModel struct { nodeData lnd.NodeData ctx context.Context loaded bool - quitting bool + base BaseModel } diff --git a/internal/tui/invoice.go b/internal/tui/invoice.go index 025fb8f..536107a 100644 --- a/internal/tui/invoice.go +++ b/internal/tui/invoice.go @@ -58,6 +58,7 @@ type InvoiceModel struct { ctx context.Context invoiceState InvoiceState dashboard *DashboardModel + base *BaseModel } // Variables for form value reference @@ -89,8 +90,9 @@ func isFormReady(v bool) error { // Invoice generation form func NewInvoiceModel(context context.Context, service *lndclient.GrpcLndServices, state InvoiceState, dashboard *DashboardModel) InvoiceModel { m := InvoiceModel{width: maxWidth, lndService: service, ctx: context, invoiceState: state, dashboard: dashboard} + m.base = NewBaseModel() m.lg = lipgloss.DefaultRenderer() - m.styles = NewDialogStyles(m.lg) + m.styles = NewStyles(m.lg) m.form = huh.NewForm( huh.NewGroup( huh.NewInput(). @@ -133,6 +135,11 @@ func (m InvoiceModel) backToDashboard() (tea.Model, tea.Cmd) { // Handle update messages for the model func (m InvoiceModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + // Base model logic + model, cmd := m.base.Update(msg) + if cmd != nil { + return model, cmd + } switch msg := msg.(type) { case tea.WindowSizeMsg: windowSizeMsg = msg @@ -145,8 +152,6 @@ func (m InvoiceModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.KeyMsg: switch { - case key.Matches(msg, Keymap.Quit): - return m, tea.Quit case key.Matches(msg, Keymap.Back): return m.backToDashboard() case key.Matches(msg, Keymap.Enter): diff --git a/internal/tui/styling.go b/internal/tui/styling.go index 706bc91..44359e6 100644 --- a/internal/tui/styling.go +++ b/internal/tui/styling.go @@ -18,15 +18,17 @@ type Styles struct { BorderedStyle, FocusedStyle, Help lipgloss.Style - Keyword func(string) string - SubKeyword func(string) string + Keyword, + SubKeyword, + NegativeString, + PositiveString func(string) string } func GetDefaultStyles() *Styles { - return NewDialogStyles( lipgloss.DefaultRenderer()) + return NewStyles(lipgloss.DefaultRenderer()) } -func NewDialogStyles(lg *lipgloss.Renderer) *Styles { +func NewStyles(lg *lipgloss.Renderer) *Styles { s := Styles{} s.Base = lg.NewStyle(). Padding(1, 4, 0, 1) @@ -51,6 +53,8 @@ func NewDialogStyles(lg *lipgloss.Renderer) *Styles { s.Keyword = makeFgStyle("211") s.SubKeyword = makeFgStyle("140") + s.NegativeString = makeFgStyle("125") + s.PositiveString = makeFgStyle("78") s.BorderedStyle = lipgloss.NewStyle(). Padding(1, 2). Border(lipgloss.RoundedBorder()).