diff --git a/ui/controls/controller.go b/ui/controls/controller.go deleted file mode 100644 index 006f9eb..0000000 --- a/ui/controls/controller.go +++ /dev/null @@ -1,29 +0,0 @@ -package controls - -import ( - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" -) - -// Init has no I/O right now -func (m Model) Init() tea.Cmd { - return nil -} - -// Update processes incoming messages, modifies the model state, and returns the updated model and command to execute. -func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - return m.HandleMessage(msg) -} - -// HandleMessage processes incoming messages, updates the model's state, and returns the updated model and a command to execute. -func (m Model) HandleMessage(msg tea.Msg) (Model, tea.Cmd) { - switch msg := msg.(type) { - - case tea.WindowSizeMsg: - if msg.Width != 0 && msg.Height != 0 { - m.Width = msg.Width - m.Height = lipgloss.Height(m.View()) - } - } - return m, nil -} diff --git a/ui/controls/controls_test.go b/ui/controls/controls_test.go deleted file mode 100644 index ed1811a..0000000 --- a/ui/controls/controls_test.go +++ /dev/null @@ -1,35 +0,0 @@ -package controls - -import ( - "bytes" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/x/exp/teatest" - "testing" - "time" -) - -func Test_Controls(t *testing.T) { - expected := "(q)uit | (d)elete | (g)enerate | (t)xn | (h)ide" - // Create the Model - m := New(expected) - - tm := teatest.NewTestModel( - t, m, - teatest.WithInitialTermSize(80, 40), - ) - - // Wait for prompt to exit - teatest.WaitFor( - t, tm.Output(), - func(bts []byte) bool { - return bytes.Contains(bts, []byte(expected)) - }, - teatest.WithCheckInterval(time.Millisecond*100), - teatest.WithDuration(time.Second*3), - ) - - // Send quit msg - tm.Send(tea.QuitMsg{}) - - tm.WaitFinished(t, teatest.WithFinalTimeout(time.Second)) -} diff --git a/ui/controls/model.go b/ui/controls/model.go deleted file mode 100644 index 92baa81..0000000 --- a/ui/controls/model.go +++ /dev/null @@ -1,19 +0,0 @@ -package controls - -// Model represents the data structure used for defining visibility, dimensions, and content. -type Model struct { - Width int - Height int - IsVisible bool - Content string -} - -// New creates a instance of a Model -func New(body string) Model { - return Model{ - IsVisible: true, - Width: 80, - Height: 24, - Content: body, - } -} diff --git a/ui/controls/style.go b/ui/controls/style.go deleted file mode 100644 index 3ad10eb..0000000 --- a/ui/controls/style.go +++ /dev/null @@ -1,12 +0,0 @@ -package controls - -import "github.com/charmbracelet/lipgloss" - -var controlStyle = func() lipgloss.Style { - b := lipgloss.RoundedBorder() - b.Left = "┤" - b.Right = "├" - return lipgloss.NewStyle(). - BorderStyle(lipgloss.RoundedBorder()). - BorderStyle(b) -}() diff --git a/ui/controls/view.go b/ui/controls/view.go deleted file mode 100644 index 3754f42..0000000 --- a/ui/controls/view.go +++ /dev/null @@ -1,17 +0,0 @@ -package controls - -import ( - "github.com/charmbracelet/lipgloss" - "strings" -) - -// View renders the model's content if it is visible, aligning it horizontally and ensuring it fits within the specified width. -func (m Model) View() string { - if !m.IsVisible { - return "" - } - render := controlStyle.Render(m.Content) - difference := m.Width - lipgloss.Width(render) - line := strings.Repeat("─", max(0, difference/2)) - return lipgloss.JoinHorizontal(lipgloss.Center, line, render, line) -} diff --git a/ui/error.go b/ui/error.go index c0678d7..f074251 100644 --- a/ui/error.go +++ b/ui/error.go @@ -1,7 +1,6 @@ package ui import ( - "github.com/algorandfoundation/hack-tui/ui/controls" "github.com/algorandfoundation/hack-tui/ui/style" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" @@ -9,10 +8,9 @@ import ( ) type ErrorViewModel struct { - Height int - Width int - controls controls.Model - Message string + Height int + Width int + Message string } func NewErrorViewModel(message string) ErrorViewModel { diff --git a/ui/pages/transaction/controller.go b/ui/pages/transaction/controller.go index 5b5eaa9..6f15570 100644 --- a/ui/pages/transaction/controller.go +++ b/ui/pages/transaction/controller.go @@ -3,12 +3,13 @@ package transaction import ( "encoding/base64" "fmt" + "github.com/algorandfoundation/hack-tui/ui/style" + "github.com/charmbracelet/lipgloss" "github.com/algorand/go-algorand-sdk/v2/types" "github.com/algorandfoundation/algourl/encoder" "github.com/algorandfoundation/hack-tui/api" tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" ) func (m ViewModel) Init() tea.Cmd { @@ -34,10 +35,10 @@ func (m *ViewModel) UpdateTxnURLAndQRCode() error { case "Not Participating": // This status means the account can never participate in consensus m.urlTxn = "" m.asciiQR = "" - m.hint = fmt.Sprintf("%s is NotParticipating. Cannot register key.", m.Data.Address) + m.hint = fmt.Sprintf("%s is NotParticipating. Cannot register key.", m.FormatedAddress()) return nil } - + m.IsOnline = isOnline fee := uint64(1000) kr := &encoder.AUrlTxn{} @@ -69,7 +70,7 @@ func (m *ViewModel) UpdateTxnURLAndQRCode() error { }, } - m.hint = fmt.Sprintf("Scan this QR code to take %s Online.", m.Data.Address) + m.hint = fmt.Sprintf("Scan this QR code to take %s Online.", m.FormatedAddress()) } else { @@ -81,7 +82,7 @@ func (m *ViewModel) UpdateTxnURLAndQRCode() error { Fee: &fee, }} - m.hint = fmt.Sprintf("Scan this QR code to take %s Offline.", m.Data.Address) + m.hint = fmt.Sprintf("Scan this QR code to take %s Offline.", m.FormatedAddress()) } qrCode, err := kr.ProduceQRCode() @@ -100,7 +101,6 @@ func (m ViewModel) HandleMessage(msg tea.Msg) (ViewModel, tea.Cmd) { var cmd tea.Cmd switch msg := msg.(type) { // When the participation key updates, set the models data - case *api.ParticipationKey: m.Data = *msg @@ -111,13 +111,9 @@ func (m ViewModel) HandleMessage(msg tea.Msg) (ViewModel, tea.Cmd) { // Handle View Size changes case tea.WindowSizeMsg: - if msg.Width != 0 && msg.Height != 0 { - m.Width = msg.Width - m.Height = max(0, msg.Height-lipgloss.Height(m.controls.View())-3) - } + borderRender := style.Border.Render("") + m.Width = max(0, msg.Width-lipgloss.Width(borderRender)) + m.Height = max(0, msg.Height-lipgloss.Height(borderRender)) } - - // Pass messages to controls - m.controls, cmd = m.controls.HandleMessage(msg) return m, cmd } diff --git a/ui/pages/transaction/model.go b/ui/pages/transaction/model.go index aed1016..be00c1f 100644 --- a/ui/pages/transaction/model.go +++ b/ui/pages/transaction/model.go @@ -1,14 +1,12 @@ package transaction import ( + "fmt" "github.com/algorandfoundation/hack-tui/api" "github.com/algorandfoundation/hack-tui/internal" - "github.com/algorandfoundation/hack-tui/ui/controls" - "github.com/charmbracelet/lipgloss" + "github.com/algorandfoundation/hack-tui/ui/style" ) -var green = lipgloss.NewStyle().Foreground(lipgloss.Color("10")) - type ViewModel struct { // Width is the last known horizontal lines Width int @@ -19,10 +17,12 @@ type ViewModel struct { Data api.ParticipationKey // Pointer to the State - State *internal.StateModel + State *internal.StateModel + IsOnline bool // Components - controls controls.Model + controls string + navigation string // QR Code, URL and hint text asciiQR string @@ -30,10 +30,16 @@ type ViewModel struct { hint string } +func (m ViewModel) FormatedAddress() string { + return fmt.Sprintf("%s...%s", m.Data.Address[0:4], m.Data.Address[len(m.Data.Address)-4:]) +} + // New creates and instance of the ViewModel with a default controls.Model func New(state *internal.StateModel) ViewModel { return ViewModel{ - State: state, - controls: controls.New(" (a)ccounts | (k)eys | " + green.Render("(t)xn") + " | shift+tab: back "), + State: state, + IsOnline: false, + navigation: "| (a)ccounts | (k)eys | " + style.Green.Render("(t)xn") + " |", + controls: "( shift+tab: back )", } } diff --git a/ui/pages/transaction/view.go b/ui/pages/transaction/view.go index 795841c..4d972c2 100644 --- a/ui/pages/transaction/view.go +++ b/ui/pages/transaction/view.go @@ -7,43 +7,56 @@ import ( ) func (m ViewModel) View() string { - qrRender := lipgloss.JoinVertical( - lipgloss.Center, - style.Yellow.Render(m.hint), - "", - qrStyle.Render(m.asciiQR), - urlStyle.Render(m.urlTxn), - ) + qrCode := qrStyle.Render(m.asciiQR) + qrWidth := lipgloss.Width(qrCode) + 1 + qrHeight := lipgloss.Height(qrCode) + title := "" + if m.IsOnline { + title = "Offline Transaction" + } else { + title = "Online Transaction" + } - if m.asciiQR == "" || m.urlTxn == "" { - return lipgloss.JoinVertical( - lipgloss.Center, - "No QR Code or TxnURL available", - "\n", - m.controls.View()) + url := "" + if lipgloss.Width(m.urlTxn) > qrWidth { + url = m.urlTxn[:(qrWidth-3)] + "..." + } else { + url = m.urlTxn } - if lipgloss.Height(qrRender) > m.Height { - padHeight := max(0, m.Height-lipgloss.Height(m.controls.View())-1) - padHString := strings.Repeat("\n", padHeight/2) + var render string + if qrWidth > m.Width || qrHeight+2 > m.Height { text := style.Red.Render("QR Code too large to display... Please adjust terminal dimensions or font.") + padHeight := max(0, m.Height-lipgloss.Height(text)) + padHString := strings.Repeat("\n", padHeight/2) padWidth := max(0, m.Width-lipgloss.Width(text)) padWString := strings.Repeat(" ", padWidth/2) - return lipgloss.JoinVertical( + paddedStr := lipgloss.JoinVertical( lipgloss.Left, padHString, lipgloss.JoinHorizontal(lipgloss.Left, padWString, style.Red.Render("QR Code too large to display... Please adjust terminal dimensions or font.")), - padHString, - m.controls.View()) - } + ) + render = style.ApplyBorder(m.Width, m.Height, "8").Render(paddedStr) + } else { + qRemainingWidth := max(0, (m.Width-lipgloss.Width(qrCode))/2) + qrCode = lipgloss.JoinHorizontal(lipgloss.Left, strings.Repeat(" ", qRemainingWidth), qrCode, strings.Repeat(" ", qRemainingWidth)) + qRemainingHeight := max(0, (m.Height-2-lipgloss.Height(qrCode))/2) + if qrHeight+2 == m.Height { + qrCode = lipgloss.JoinVertical(lipgloss.Center, style.Yellow.Render(m.hint), qrCode, urlStyle.Render(url)) + } else { + qrCode = lipgloss.JoinVertical(lipgloss.Center, strings.Repeat("\n", qRemainingHeight), style.Yellow.Render(m.hint), qrCode, urlStyle.Render(url)) - qrRenderPadHeight := max(0, m.Height-(lipgloss.Height(qrRender)-lipgloss.Height(m.controls.View()))-1) - qrPad := strings.Repeat("\n", qrRenderPadHeight/2) - return lipgloss.JoinVertical( - lipgloss.Center, - qrPad, - qrRender, - qrPad, - m.controls.View(), + } + render = style.ApplyBorder(m.Width, m.Height, "8").Render(qrCode) + } + return style.WithNavigation( + m.navigation, + style.WithControls( + m.controls, + style.WithTitle( + title, + render, + ), + ), ) }