diff --git a/api/lf.go b/api/lf.go index def9282..90a18c3 100644 --- a/api/lf.go +++ b/api/lf.go @@ -1,6 +1,6 @@ // Package api provides primitives to interact with the openapi HTTP API. // -// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.4.1 DO NOT EDIT. +// Code generated by github.com/deepmap/oapi-codegen/v2 version v2.2.0 DO NOT EDIT. package api import ( diff --git a/cmd/root.go b/cmd/root.go index 6ad5f43..1f04e8e 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -6,6 +6,7 @@ import ( "github.com/algorandfoundation/hack-tui/api" "github.com/algorandfoundation/hack-tui/internal" "github.com/algorandfoundation/hack-tui/ui" + "github.com/algorandfoundation/hack-tui/ui/style" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/log" "github.com/oapi-codegen/oapi-codegen/v2/pkg/securityprovider" @@ -32,7 +33,7 @@ var ( rootCmd = &cobra.Command{ Use: "algorun", Short: "Manage Algorand nodes", - Long: ui.Purple(BANNER) + "\n", + Long: style.Purple(BANNER) + "\n", CompletionOptions: cobra.CompletionOptions{ DisableDefaultCmd: true, }, @@ -61,7 +62,7 @@ var ( }, ParticipationKeys: partkeys, } - state.Accounts = internal.AccountsFromState(&state, client) + state.Accounts = internal.AccountsFromState(&state, new(internal.Clock), client) // Fetch current state err = state.Status.Fetch(context.Background(), client) @@ -73,6 +74,7 @@ var ( p := tea.NewProgram( m, tea.WithAltScreen(), + tea.WithFPS(120), ) go func() { state.Watch(func(status *internal.StateModel, err error) { @@ -112,19 +114,19 @@ func init() { rootCmd.Version = Version // Bindings - rootCmd.PersistentFlags().StringVar(&server, "server", "", ui.LightBlue("server address")) - rootCmd.PersistentFlags().StringVar(&token, "token", "", ui.LightBlue("server token")) + rootCmd.PersistentFlags().StringVar(&server, "server", "", style.LightBlue("server address")) + rootCmd.PersistentFlags().StringVar(&token, "token", "", style.LightBlue("server token")) _ = viper.BindPFlag("server", rootCmd.PersistentFlags().Lookup("server")) _ = viper.BindPFlag("token", rootCmd.PersistentFlags().Lookup("token")) // Update Long Text rootCmd.Long += - ui.Magenta("Configuration: ") + viper.GetViper().ConfigFileUsed() + "\n" + - ui.LightBlue("Server: ") + viper.GetString("server") + style.Magenta("Configuration: ") + viper.GetViper().ConfigFileUsed() + "\n" + + style.LightBlue("Server: ") + viper.GetString("server") if viper.GetString("data") != "" { rootCmd.Long += - ui.Magenta("\nAlgorand Data: ") + viper.GetString("data") + style.Magenta("\nAlgorand Data: ") + viper.GetString("data") } // Add Commands diff --git a/cmd/status.go b/cmd/status.go index 3c0b635..3bff01e 100644 --- a/cmd/status.go +++ b/cmd/status.go @@ -6,6 +6,7 @@ import ( "fmt" "github.com/algorandfoundation/hack-tui/internal" "github.com/algorandfoundation/hack-tui/ui" + "github.com/algorandfoundation/hack-tui/ui/style" tea "github.com/charmbracelet/bubbletea" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -16,10 +17,10 @@ import ( var statusCmd = &cobra.Command{ Use: "status", Short: "Get the node status", - Long: ui.Purple(BANNER) + "\n" + ui.LightBlue("View the node status"), + Long: style.Purple(BANNER) + "\n" + style.LightBlue("View the node status"), RunE: func(cmd *cobra.Command, args []string) error { if viper.GetString("server") == "" { - return errors.New(ui.Magenta("server is required")) + return errors.New(style.Magenta("server is required")) } // Get Algod from configuration diff --git a/internal/accounts.go b/internal/accounts.go index 4a0e4fd..7169f82 100644 --- a/internal/accounts.go +++ b/internal/accounts.go @@ -25,8 +25,6 @@ type Account struct { Keys int // Expires is the date the participation key will expire Expires time.Time - // The LastModified round, this only pertains to keys that can be updated - LastModified int } // Gets the list of addresses created at genesis from the genesis file @@ -92,17 +90,8 @@ func getAddressesFromGenesis(client *api.ClientWithResponses) ([]string, string, return addresses, rewardsPool, feeSink, nil } -func isValidStatus(status string) bool { - validStatuses := map[string]bool{ - "Online": true, - "Offline": true, - "Not Participating": true, - } - return validStatuses[status] -} - // Get Online Status of Account -func getAccountOnlineStatus(client *api.ClientWithResponses, address string) (string, error) { +func GetAccount(client *api.ClientWithResponses, address string) (api.Account, error) { var format api.AccountInformationParamsFormat = "json" r, err := client.AccountInformationWithResponse( context.Background(), @@ -111,23 +100,20 @@ func getAccountOnlineStatus(client *api.ClientWithResponses, address string) (st Format: &format, }) + var accountInfo api.Account if err != nil { - return "N/A", err + return accountInfo, err } if r.StatusCode() != 200 { - return "N/A", errors.New(fmt.Sprintf("Failed to get account information. Received error code: %d", r.StatusCode())) + return accountInfo, errors.New(fmt.Sprintf("Failed to get account information. Received error code: %d", r.StatusCode())) } - if r.JSON200 == nil { - return "N/A", errors.New("Failed to get account information. JSON200 is nil") - } - - return r.JSON200.Status, nil + return *r.JSON200, nil } // AccountsFromParticipationKeys maps an array of api.ParticipationKey to a keyed map of Account -func AccountsFromState(state *StateModel, client *api.ClientWithResponses) map[string]Account { +func AccountsFromState(state *StateModel, t Time, client *api.ClientWithResponses) map[string]Account { values := make(map[string]Account) if state == nil || state.ParticipationKeys == nil { return values @@ -136,18 +122,27 @@ func AccountsFromState(state *StateModel, client *api.ClientWithResponses) map[s val, ok := values[key.Address] if !ok { - statusOnline, err := getAccountOnlineStatus(client, key.Address) + account, err := GetAccount(client, key.Address) + // TODO: handle error if err != nil { // TODO: Logging panic(err) } + var expires = t.Now() + if key.EffectiveLastValid != nil { + now := t.Now() + roundDiff := max(0, *key.EffectiveLastValid-int(state.Status.LastRound)) + distance := int(state.Metrics.RoundTime) * roundDiff + expires = now.Add(time.Duration(distance)) + } + values[key.Address] = Account{ Address: key.Address, - Status: statusOnline, - Balance: 0, - Expires: time.Unix(0, 0), + Status: account.Status, + Balance: account.Amount / 1000000, + Expires: expires, Keys: 1, } } else { diff --git a/internal/accounts_test.go b/internal/accounts_test.go index a4fecc7..234bf7f 100644 --- a/internal/accounts_test.go +++ b/internal/accounts_test.go @@ -1,14 +1,17 @@ package internal import ( - "testing" - "time" - "github.com/algorandfoundation/hack-tui/api" "github.com/oapi-codegen/oapi-codegen/v2/pkg/securityprovider" "github.com/stretchr/testify/assert" + "testing" + "time" ) +type TestClock struct{} + +func (TestClock) Now() time.Time { return time.Time{} } + func Test_AccountsFromState(t *testing.T) { // Setup elevated client @@ -24,67 +27,68 @@ func Test_AccountsFromState(t *testing.T) { t.Fatal(err) } - // Test getAccountOnlineStatus - - var mapAddressOnlineStatus = make(map[string]string) - + var mapAccounts = make(map[string]api.Account) + var onlineAccounts = make([]api.Account, 0) for _, address := range addresses { - status, err := getAccountOnlineStatus(client, address) + acct, err := GetAccount(client, address) if err != nil { t.Fatal(err) } - assert.True(t, status == "Online" || status == "Offline") - mapAddressOnlineStatus[address] = status + assert.True(t, acct.Status == "Online" || acct.Status == "Offline") + mapAccounts[address] = acct + if acct.Status == "Online" { + onlineAccounts = append(onlineAccounts, acct) + } } - status, err := getAccountOnlineStatus(client, rewardsPool) + acct, err := GetAccount(client, rewardsPool) if err != nil { t.Fatal(err) } - if status != "Not Participating" { - t.Fatalf("Expected RewardsPool to be 'Not Participating', got %s", status) + if acct.Status != "Not Participating" { + t.Fatalf("Expected RewardsPool to be 'Not Participating', got %s", acct.Status) } - status, err = getAccountOnlineStatus(client, feeSink) + acct, err = GetAccount(client, feeSink) if err != nil { t.Fatal(err) } - if status != "Not Participating" { - t.Fatalf("Expected FeeSink to be 'Not Participating', got %s", status) + if acct.Status != "Not Participating" { + t.Fatalf("Expected FeeSink to be 'Not Participating', got %s", acct.Status) } - _, err = getAccountOnlineStatus(client, "invalid_address") + _, err = GetAccount(client, "invalid_address") if err == nil { t.Fatal("Expected error for invalid address") } - // Test AccountFromState + // Test Account from State - // Prepare expected results - // Only include addresses with "Online" status - onlineAddresses := make(map[string]string) - for address, status := range mapAddressOnlineStatus { - if status == "Online" { - onlineAddresses[address] = status - } - } + effectiveFirstValid := 0 + effectiveLastValid := 10000 - // Create expectedAccounts dynamically from Online accounts, and mocked participation keys - mockedPartKeys := make([]api.ParticipationKey, 0) - expectedAccounts := make(map[string]Account) - for address, status := range onlineAddresses { - expectedAccounts[address] = Account{ - Address: address, - Status: status, - Balance: 0, - Expires: time.Unix(0, 0), - Keys: 1, - LastModified: 0, - } - - mockedPartKeys = append(mockedPartKeys, api.ParticipationKey{ - Address: address, + // Create mockedPart Keys + var mockedPartKeys = []api.ParticipationKey{ + { + Address: onlineAccounts[0].Address, + EffectiveFirstValid: &effectiveFirstValid, + EffectiveLastValid: &effectiveLastValid, + Id: "", + Key: api.AccountParticipation{ + SelectionParticipationKey: nil, + StateProofKey: nil, + VoteParticipationKey: nil, + VoteFirstValid: 0, + VoteLastValid: 9999999, + VoteKeyDilution: 0, + }, + LastBlockProposal: nil, + LastStateProof: nil, + LastVote: nil, + }, + { + Address: onlineAccounts[0].Address, EffectiveFirstValid: nil, EffectiveLastValid: nil, Id: "", @@ -99,16 +103,74 @@ func Test_AccountsFromState(t *testing.T) { LastBlockProposal: nil, LastStateProof: nil, LastVote: nil, - }) + }, + { + Address: onlineAccounts[1].Address, + EffectiveFirstValid: &effectiveFirstValid, + EffectiveLastValid: &effectiveLastValid, + Id: "", + Key: api.AccountParticipation{ + SelectionParticipationKey: nil, + StateProofKey: nil, + VoteParticipationKey: nil, + VoteFirstValid: 0, + VoteLastValid: 9999999, + VoteKeyDilution: 0, + }, + LastBlockProposal: nil, + LastStateProof: nil, + LastVote: nil, + }, } // Mock StateModel state := &StateModel{ + Metrics: MetricsModel{ + Enabled: true, + Window: 100, + RoundTime: time.Duration(2) * time.Second, + TPS: 20, + RX: 1024, + TX: 2048, + }, + Status: StatusModel{ + State: "WATCHING", + Version: "v0.0.0-test", + Network: "tuinet", + Voting: false, + NeedsUpdate: false, + LastRound: 1337, + }, ParticipationKeys: &mockedPartKeys, } + // Calculate expiration + clock := new(TestClock) + now := clock.Now() + roundDiff := max(0, effectiveLastValid-int(state.Status.LastRound)) + distance := int(state.Metrics.RoundTime) * roundDiff + expires := now.Add(time.Duration(distance)) + + // Construct expected accounts + expectedAccounts := map[string]Account{ + onlineAccounts[0].Address: { + Address: onlineAccounts[0].Address, + Status: onlineAccounts[0].Status, + Balance: onlineAccounts[0].Amount / 1_000_000, + Keys: 2, + Expires: expires, + }, + onlineAccounts[1].Address: { + Address: onlineAccounts[1].Address, + Status: onlineAccounts[1].Status, + Balance: onlineAccounts[1].Amount / 1_000_000, + Keys: 1, + Expires: expires, + }, + } + // Call AccountsFromState - accounts := AccountsFromState(state, client) + accounts := AccountsFromState(state, clock, client) // Assert results assert.Equal(t, expectedAccounts, accounts) diff --git a/internal/block.go b/internal/block.go index 7f839a4..5af2fba 100644 --- a/internal/block.go +++ b/internal/block.go @@ -7,23 +7,6 @@ import ( "time" ) -func GetBlock(ctx context.Context, client *api.ClientWithResponses, round uint64) (map[string]interface{}, error) { - - var format api.GetBlockParamsFormat = "json" - block, err := client.GetBlockWithResponse(ctx, int(round), &api.GetBlockParams{ - Format: &format, - }) - if err != nil { - return nil, err - } - - if block.StatusCode() != 200 { - return nil, errors.New(block.Status()) - } - - return block.JSON200.Block, nil -} - type BlockMetrics struct { AvgTime time.Duration TPS float64 @@ -58,8 +41,14 @@ func GetBlockMetrics(ctx context.Context, client *api.ClientWithResponses, round } // Push to the transactions count list - aTimestamp := time.Duration(a.JSON200.Block["ts"].(float64)) * time.Second - bTimestamp := time.Duration(b.JSON200.Block["ts"].(float64)) * time.Second + aTimestampRes := a.JSON200.Block["ts"] + bTimestampRes := b.JSON200.Block["ts"] + if aTimestampRes == nil || bTimestampRes == nil { + return &avgs, nil + } + aTimestamp := time.Duration(aTimestampRes.(float64)) * time.Second + bTimestamp := time.Duration(bTimestampRes.(float64)) * time.Second + // Transaction Counter aTransactions := a.JSON200.Block["tc"] bTransactions := b.JSON200.Block["tc"] diff --git a/internal/state.go b/internal/state.go index 6cae09e..c20508c 100644 --- a/internal/state.go +++ b/internal/state.go @@ -96,7 +96,7 @@ func (s *StateModel) UpdateMetricsFromRPC(ctx context.Context, client *api.Clien } } func (s *StateModel) UpdateAccounts(client *api.ClientWithResponses) { - s.Accounts = AccountsFromState(s, client) + s.Accounts = AccountsFromState(s, new(Clock), client) } func (s *StateModel) UpdateKeys(ctx context.Context, client *api.ClientWithResponses) { diff --git a/internal/time.go b/internal/time.go new file mode 100644 index 0000000..eb3fa8f --- /dev/null +++ b/internal/time.go @@ -0,0 +1,11 @@ +package internal + +import "time" + +type Time interface { + Now() time.Time +} + +type Clock struct{} + +func (Clock) Now() time.Time { return time.Now() } diff --git a/ui/error.go b/ui/error.go index e690634..c0678d7 100644 --- a/ui/error.go +++ b/ui/error.go @@ -2,6 +2,7 @@ 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" "strings" @@ -16,9 +17,9 @@ type ErrorViewModel struct { func NewErrorViewModel(message string) ErrorViewModel { return ErrorViewModel{ - Height: 0, - Width: 0, - controls: controls.New(" Error "), + Height: 0, + Width: 0, + Message: message, } } @@ -35,14 +36,34 @@ func (m ErrorViewModel) HandleMessage(msg tea.Msg) (ErrorViewModel, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: - m.Width = msg.Width - m.Height = msg.Height - 2 + borderRender := style.Border.Render("") + m.Width = max(0, msg.Width-lipgloss.Width(borderRender)) + m.Height = max(0, msg.Height-lipgloss.Height(borderRender)) } - m.controls, cmd = m.controls.HandleMessage(msg) + return m, cmd } func (m ErrorViewModel) View() string { - pad := strings.Repeat("\n", max(0, m.Height/2-1)) - return lipgloss.JoinVertical(lipgloss.Center, pad, red.Render(m.Message), pad, m.controls.View()) + msgHeight := lipgloss.Height(m.Message) + msgWidth := lipgloss.Width(m.Message) + + if msgWidth > m.Width/2 { + m.Message = m.Message[0:m.Width/2] + "..." + msgWidth = m.Width/2 + 3 + } + + msg := style.Red.Render(m.Message) + padT := strings.Repeat("\n", max(0, (m.Height/2)-msgHeight)) + padL := strings.Repeat(" ", max(0, (m.Width-msgWidth)/2)) + + text := lipgloss.JoinHorizontal(lipgloss.Left, padL, msg) + render := style.ApplyBorder(m.Width, m.Height, "8").Render(lipgloss.JoinVertical(lipgloss.Center, padT, text)) + return style.WithNavigation( + "( Waiting for recovery... )", + style.WithTitle( + "System Error", + render, + ), + ) } diff --git a/ui/pages/accounts/controller.go b/ui/pages/accounts/controller.go index b787721..434f0ea 100644 --- a/ui/pages/accounts/controller.go +++ b/ui/pages/accounts/controller.go @@ -2,7 +2,7 @@ package accounts import ( "github.com/algorandfoundation/hack-tui/internal" - "github.com/algorandfoundation/hack-tui/ui/pages" + "github.com/algorandfoundation/hack-tui/ui/style" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) @@ -31,14 +31,19 @@ func (m ViewModel) HandleMessage(msg tea.Msg) (ViewModel, tea.Cmd) { return m, tea.Quit } case tea.WindowSizeMsg: - m.table.SetWidth(msg.Width - lipgloss.Width(pages.Padding1("")) - 4) - m.table.SetHeight(msg.Height - lipgloss.Height(pages.Padding1("")) - lipgloss.Height(m.controls.View())) - m.table.SetColumns(m.makeColumns(msg.Width - lipgloss.Width(pages.Padding1("")) - 14)) + borderRender := style.Border.Render("") + borderWidth := lipgloss.Width(borderRender) + borderHeight := lipgloss.Height(borderRender) + + m.Width = max(0, msg.Width-borderWidth) + m.Height = max(0, msg.Height-borderHeight) + + m.table.SetWidth(m.Width) + m.table.SetHeight(max(0, m.Height)) + m.table.SetColumns(m.makeColumns(m.Width)) } m.table, cmd = m.table.Update(msg) cmds = append(cmds, cmd) - m.controls, cmd = m.controls.HandleMessage(msg) - cmds = append(cmds, cmd) return m, tea.Batch(cmds...) } diff --git a/ui/pages/accounts/model.go b/ui/pages/accounts/model.go index cfbc6f9..425adf4 100644 --- a/ui/pages/accounts/model.go +++ b/ui/pages/accounts/model.go @@ -1,12 +1,11 @@ package accounts import ( + "github.com/algorandfoundation/hack-tui/ui/style" "sort" "strconv" "github.com/algorandfoundation/hack-tui/internal" - "github.com/algorandfoundation/hack-tui/ui/controls" - "github.com/algorandfoundation/hack-tui/ui/pages" "github.com/charmbracelet/bubbles/table" "github.com/charmbracelet/lipgloss" ) @@ -16,16 +15,18 @@ type ViewModel struct { Height int Data map[string]internal.Account - table table.Model - controls controls.Model + table table.Model + navigation string + controls string } func New(state *internal.StateModel) ViewModel { m := ViewModel{ - Width: 0, - Height: 0, - Data: state.Accounts, - controls: controls.New(" (g)enerate | " + green.Render("(a)ccounts") + " | (k)eys | (t)xn "), + Width: 0, + Height: 0, + Data: state.Accounts, + controls: "( (g)enerate )", + navigation: "| " + style.Green.Render("(a)ccounts") + " | (k)eys | (t)xn |", } m.table = table.New( @@ -56,7 +57,7 @@ func (m ViewModel) SelectedAccount() internal.Account { return account } func (m ViewModel) makeColumns(width int) []table.Column { - avgWidth := (width - lipgloss.Width(pages.Padding1("")) - 14) / 5 + avgWidth := (width - lipgloss.Width(style.Border.Render("")) - 14) / 5 return []table.Column{ {Title: "Account", Width: avgWidth}, {Title: "Keys", Width: avgWidth}, diff --git a/ui/pages/accounts/style.go b/ui/pages/accounts/style.go deleted file mode 100644 index d55024a..0000000 --- a/ui/pages/accounts/style.go +++ /dev/null @@ -1,5 +0,0 @@ -package accounts - -import "github.com/charmbracelet/lipgloss" - -var green = lipgloss.NewStyle().Foreground(lipgloss.Color("10")) diff --git a/ui/pages/accounts/view.go b/ui/pages/accounts/view.go index dcdcffb..5691335 100644 --- a/ui/pages/accounts/view.go +++ b/ui/pages/accounts/view.go @@ -1,10 +1,19 @@ package accounts import ( - "github.com/algorandfoundation/hack-tui/ui/pages" - "github.com/charmbracelet/lipgloss" + "github.com/algorandfoundation/hack-tui/ui/style" ) func (m ViewModel) View() string { - return lipgloss.JoinVertical(lipgloss.Center, pages.Padding1(m.table.View()), m.controls.View()) + table := style.ApplyBorder(m.Width, m.Height, "8").Render(m.table.View()) + return style.WithNavigation( + m.navigation, + style.WithControls( + m.controls, + style.WithTitle( + "Accounts", + table, + ), + ), + ) } diff --git a/ui/pages/generate/cmds.go b/ui/pages/generate/cmds.go new file mode 100644 index 0000000..6690034 --- /dev/null +++ b/ui/pages/generate/cmds.go @@ -0,0 +1,14 @@ +package generate + +import ( + tea "github.com/charmbracelet/bubbletea" +) + +type Cancel struct{} + +// EmitCancelGenerate cancel generation +func EmitCancel(cg Cancel) tea.Cmd { + return func() tea.Msg { + return cg + } +} diff --git a/ui/pages/generate/controller.go b/ui/pages/generate/controller.go index 5b4dd97..c8adbaa 100644 --- a/ui/pages/generate/controller.go +++ b/ui/pages/generate/controller.go @@ -5,9 +5,10 @@ import ( "github.com/algorandfoundation/hack-tui/api" "github.com/algorandfoundation/hack-tui/internal" "github.com/algorandfoundation/hack-tui/ui/pages/accounts" - "github.com/charmbracelet/bubbles/cursor" + "github.com/algorandfoundation/hack-tui/ui/style" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/log" "strconv" ) @@ -25,18 +26,13 @@ func (m ViewModel) HandleMessage(msg tea.Msg) (ViewModel, tea.Cmd) { //var cmd tea.Cmd switch msg := msg.(type) { + case tea.WindowSizeMsg: + m.Width = msg.Width - lipgloss.Width(style.Border.Render("")) + m.Height = msg.Height - lipgloss.Height(style.Border.Render("")) case tea.KeyMsg: switch msg.String() { - case "ctrl+r": - m.cursorMode++ - if m.cursorMode > cursor.CursorHide { - m.cursorMode = cursor.CursorBlink - } - cmds := make([]tea.Cmd, len(m.Inputs)) - for i := range m.Inputs { - cmds[i] = m.Inputs[i].Cursor.SetMode(m.cursorMode) - } - return m, tea.Batch(cmds...) + case "ctrl+c", "esc": + return m, EmitCancel(Cancel{}) case "tab", "shift+tab", "up", "down": s := msg.String() diff --git a/ui/pages/generate/model.go b/ui/pages/generate/model.go index c653df6..0d7f806 100644 --- a/ui/pages/generate/model.go +++ b/ui/pages/generate/model.go @@ -7,8 +7,13 @@ import ( ) type ViewModel struct { - Address string - Inputs []textinput.Model + Width int + Height int + Address string + Inputs []textinput.Model + + controls string + client *api.ClientWithResponses focusIndex int cursorMode cursor.Mode @@ -16,9 +21,10 @@ type ViewModel struct { func New(address string, client *api.ClientWithResponses) ViewModel { m := ViewModel{ - Address: address, - Inputs: make([]textinput.Model, 3), - client: client, + Address: address, + Inputs: make([]textinput.Model, 3), + controls: "( ctrl+c to cancel )", + client: client, } var t textinput.Model diff --git a/ui/pages/generate/style.go b/ui/pages/generate/style.go index ad68d46..8783d05 100644 --- a/ui/pages/generate/style.go +++ b/ui/pages/generate/style.go @@ -6,12 +6,10 @@ import ( ) var ( - focusedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("205")) - blurredStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("240")) - cursorStyle = focusedStyle - noStyle = lipgloss.NewStyle() - helpStyle = blurredStyle - cursorModeHelpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("244")) + focusedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("205")) + blurredStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("240")) + cursorStyle = focusedStyle + noStyle = lipgloss.NewStyle() focusedButton = focusedStyle.Render("[ Submit ]") blurredButton = fmt.Sprintf("[ %s ]", blurredStyle.Render("Submit")) diff --git a/ui/pages/generate/view.go b/ui/pages/generate/view.go index b3a5c4c..667410e 100644 --- a/ui/pages/generate/view.go +++ b/ui/pages/generate/view.go @@ -2,6 +2,7 @@ package generate import ( "fmt" + "github.com/algorandfoundation/hack-tui/ui/style" "strings" ) @@ -21,9 +22,12 @@ func (m ViewModel) View() string { } fmt.Fprintf(&b, "\n\n%s\n\n", *button) - b.WriteString(helpStyle.Render("cursor mode is ")) - b.WriteString(cursorModeHelpStyle.Render(m.cursorMode.String())) - b.WriteString(helpStyle.Render(" (ctrl+r to change style)")) - - return b.String() + render := style.ApplyBorder(m.Width, m.Height, "8").Render(b.String()) + return style.WithControls( + m.controls, + style.WithTitle( + "Generate", + render, + ), + ) } diff --git a/ui/pages/keys/controller.go b/ui/pages/keys/controller.go index 7f406f3..76b9389 100644 --- a/ui/pages/keys/controller.go +++ b/ui/pages/keys/controller.go @@ -2,7 +2,7 @@ package keys import ( "github.com/algorandfoundation/hack-tui/internal" - "github.com/algorandfoundation/hack-tui/ui/pages" + "github.com/algorandfoundation/hack-tui/ui/style" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) @@ -37,16 +37,20 @@ func (m ViewModel) HandleMessage(msg tea.Msg) (ViewModel, tea.Cmd) { } case tea.WindowSizeMsg: - m.table.SetWidth(msg.Width - lipgloss.Width(pages.Padding1("")) - 4) - m.table.SetHeight(msg.Height - lipgloss.Height(pages.Padding1("")) - lipgloss.Height(m.controls.View())) - m.table.SetColumns(m.makeColumns(msg.Width - lipgloss.Width(pages.Padding1("")) - 14)) + borderRender := style.Border.Render("") + borderWidth := lipgloss.Width(borderRender) + borderHeight := lipgloss.Height(borderRender) + + m.Width = max(0, msg.Width-borderWidth) + m.Height = max(0, msg.Height-borderHeight) + m.table.SetWidth(m.Width) + m.table.SetHeight(m.Height) + m.table.SetColumns(m.makeColumns(m.Width)) } var cmds []tea.Cmd var cmd tea.Cmd m.table, cmd = m.table.Update(msg) cmds = append(cmds, cmd) - m.controls, cmd = m.controls.HandleMessage(msg) - cmds = append(cmds, cmd) return m, tea.Batch(cmds...) } diff --git a/ui/pages/keys/model.go b/ui/pages/keys/model.go index 7bf8580..836ada5 100644 --- a/ui/pages/keys/model.go +++ b/ui/pages/keys/model.go @@ -1,11 +1,10 @@ package keys import ( + "github.com/algorandfoundation/hack-tui/ui/style" "sort" "github.com/algorandfoundation/hack-tui/api" - "github.com/algorandfoundation/hack-tui/ui/controls" - "github.com/algorandfoundation/hack-tui/ui/pages" "github.com/algorandfoundation/hack-tui/ui/utils" "github.com/charmbracelet/bubbles/table" "github.com/charmbracelet/lipgloss" @@ -17,8 +16,9 @@ type ViewModel struct { Width int Height int - table table.Model - controls controls.Model + table table.Model + controls string + navigation string } func New(address string, keys *[]api.ParticipationKey) ViewModel { @@ -28,7 +28,8 @@ func New(address string, keys *[]api.ParticipationKey) ViewModel { Width: 80, Height: 24, - controls: controls.New(" (g)enerate | (a)ccounts | " + green.Render("(k)eys") + " | (t)xn | (d)elete "), + controls: "( (g)enerate | (d)elete )", + navigation: "| (a)ccounts | " + style.Green.Render("(k)eys") + " | (t)xn |", table: table.New(), } @@ -36,7 +37,7 @@ func New(address string, keys *[]api.ParticipationKey) ViewModel { table.WithColumns(m.makeColumns(80)), table.WithRows(m.makeRows(keys)), table.WithFocused(true), - table.WithHeight(m.Height-lipgloss.Height(m.controls.View())-1), + table.WithHeight(m.Height), table.WithWidth(m.Width), ) @@ -70,21 +71,23 @@ func (m ViewModel) SelectedKey() *api.ParticipationKey { } func (m ViewModel) makeColumns(width int) []table.Column { // TODO: refine responsiveness - avgWidth := (width - lipgloss.Width(pages.Padding1("")) - 14) / 13 + avgWidth := (width - lipgloss.Width(style.Border.Render("")) - 14) / 7 + + //avgWidth := 1 return []table.Column{ {Title: "ID", Width: avgWidth}, {Title: "Address", Width: avgWidth}, - {Title: "SelectionParticipationKey", Width: avgWidth}, - {Title: "VoteParticipationKey", Width: avgWidth}, - {Title: "StateProofKey", Width: avgWidth}, + {Title: "SelectionParticipationKey", Width: 0}, + {Title: "VoteParticipationKey", Width: 0}, + {Title: "StateProofKey", Width: 0}, {Title: "VoteFirstValid", Width: avgWidth}, {Title: "VoteLastValid", Width: avgWidth}, {Title: "VoteKeyDilution", Width: avgWidth}, - {Title: "EffectiveLastValid", Width: avgWidth}, - {Title: "EffectiveFirstValid", Width: avgWidth}, + {Title: "EffectiveLastValid", Width: 0}, + {Title: "EffectiveFirstValid", Width: 0}, {Title: "LastVote", Width: avgWidth}, {Title: "LastBlockProposal", Width: avgWidth}, - {Title: "LastStateProof", Width: avgWidth}, + {Title: "LastStateProof", Width: 0}, } } diff --git a/ui/pages/keys/style.go b/ui/pages/keys/style.go deleted file mode 100644 index b441f66..0000000 --- a/ui/pages/keys/style.go +++ /dev/null @@ -1,5 +0,0 @@ -package keys - -import "github.com/charmbracelet/lipgloss" - -var green = lipgloss.NewStyle().Foreground(lipgloss.Color("10")) diff --git a/ui/pages/keys/view.go b/ui/pages/keys/view.go index f249f57..595de2d 100644 --- a/ui/pages/keys/view.go +++ b/ui/pages/keys/view.go @@ -1,10 +1,19 @@ package keys import ( - "github.com/algorandfoundation/hack-tui/ui/pages" - "github.com/charmbracelet/lipgloss" + "github.com/algorandfoundation/hack-tui/ui/style" ) func (m ViewModel) View() string { - return lipgloss.JoinVertical(lipgloss.Center, pages.Padding1(m.table.View()), m.controls.View()) + table := style.ApplyBorder(m.Width, m.Height, "8").Render(m.table.View()) + return style.WithNavigation( + m.navigation, + style.WithControls( + m.controls, + style.WithTitle( + "Keys", + table, + ), + ), + ) } diff --git a/ui/pages/style.go b/ui/pages/style.go deleted file mode 100644 index 118f51e..0000000 --- a/ui/pages/style.go +++ /dev/null @@ -1,5 +0,0 @@ -package pages - -import "github.com/charmbracelet/lipgloss" - -var Padding1 = lipgloss.NewStyle().Padding(1).Render diff --git a/ui/pages/transaction/style.go b/ui/pages/transaction/style.go index 7b72e85..4e4151d 100644 --- a/ui/pages/transaction/style.go +++ b/ui/pages/transaction/style.go @@ -7,11 +7,3 @@ var qrStyle = lipgloss.NewStyle(). Background(lipgloss.Color("0")) var urlStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#2596be")) - -var red = lipgloss.NewStyle(). - Foreground(lipgloss.Color("9")) - -var yellow = lipgloss.NewStyle(). - Foreground(lipgloss.Color("11")) - -var Padding1 = lipgloss.NewStyle().Padding() diff --git a/ui/pages/transaction/view.go b/ui/pages/transaction/view.go index c1a6a54..795841c 100644 --- a/ui/pages/transaction/view.go +++ b/ui/pages/transaction/view.go @@ -1,6 +1,7 @@ package transaction import ( + "github.com/algorandfoundation/hack-tui/ui/style" "github.com/charmbracelet/lipgloss" "strings" ) @@ -8,7 +9,7 @@ import ( func (m ViewModel) View() string { qrRender := lipgloss.JoinVertical( lipgloss.Center, - yellow.Render(m.hint), + style.Yellow.Render(m.hint), "", qrStyle.Render(m.asciiQR), urlStyle.Render(m.urlTxn), @@ -25,13 +26,13 @@ func (m ViewModel) View() string { if lipgloss.Height(qrRender) > m.Height { padHeight := max(0, m.Height-lipgloss.Height(m.controls.View())-1) padHString := strings.Repeat("\n", padHeight/2) - text := red.Render("QR Code too large to display... Please adjust terminal dimensions or font.") + text := style.Red.Render("QR Code too large to display... Please adjust terminal dimensions or font.") padWidth := max(0, m.Width-lipgloss.Width(text)) padWString := strings.Repeat(" ", padWidth/2) return lipgloss.JoinVertical( lipgloss.Left, padHString, - lipgloss.JoinHorizontal(lipgloss.Left, padWString, red.Render("QR Code too large to display... Please adjust terminal dimensions or font.")), + lipgloss.JoinHorizontal(lipgloss.Left, padWString, style.Red.Render("QR Code too large to display... Please adjust terminal dimensions or font.")), padHString, m.controls.View()) } diff --git a/ui/protocol.go b/ui/protocol.go index cfbcb52..7126e6b 100644 --- a/ui/protocol.go +++ b/ui/protocol.go @@ -2,6 +2,7 @@ package ui import ( "github.com/algorandfoundation/hack-tui/internal" + "github.com/algorandfoundation/hack-tui/ui/style" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "strconv" @@ -39,14 +40,6 @@ func (m ProtocolViewModel) HandleMessage(msg tea.Msg) (ProtocolViewModel, tea.Cm m.TerminalWidth = msg.Width m.TerminalHeight = msg.Height return m, nil - case tea.KeyMsg: - switch msg.String() { - // The H key should hide the render - case "h": - m.IsVisible = !m.IsVisible - case "ctrl+c": - return m, tea.Quit - } } // Return the updated model to the Bubble Tea runtime for processing. return m, nil @@ -60,7 +53,7 @@ func (m ProtocolViewModel) View() string { if m.TerminalWidth <= 0 { return "Loading...\n\n\n\n\n\n" } - beginning := blue.Render(" Node: ") + m.Data.Version + beginning := style.Blue.Render(" Node: ") + m.Data.Version isCompact := m.TerminalWidth < 90 @@ -70,7 +63,7 @@ func (m ProtocolViewModel) View() string { end := "" if m.Data.NeedsUpdate && !isCompact { - end += green.Render("[UPDATE AVAILABLE] ") + end += style.Green.Render("[UPDATE AVAILABLE] ") } var size int @@ -88,16 +81,16 @@ func (m ProtocolViewModel) View() string { if !isCompact { rows = append(rows, "") } - rows = append(rows, blue.Render(" Network: ")+m.Data.Network) + rows = append(rows, style.Blue.Render(" Network: ")+m.Data.Network) if !isCompact { rows = append(rows, "") } - rows = append(rows, blue.Render(" Protocol Voting: ")+strconv.FormatBool(m.Data.Voting)) + rows = append(rows, style.Blue.Render(" Protocol Voting: ")+strconv.FormatBool(m.Data.Voting)) if isCompact && m.Data.NeedsUpdate { - rows = append(rows, blue.Render(" Upgrade Available: ")+green.Render(strconv.FormatBool(m.Data.NeedsUpdate))) + rows = append(rows, style.Blue.Render(" Upgrade Available: ")+style.Green.Render(strconv.FormatBool(m.Data.NeedsUpdate))) } - return WithTitle("Protocol", topSections(max(0, size)).Render(lipgloss.JoinVertical(lipgloss.Left, + return style.WithTitle("Protocol", style.ApplyBorder(max(0, size-2), 5, "5").Render(lipgloss.JoinVertical(lipgloss.Left, rows..., ))) } diff --git a/ui/protocol_test.go b/ui/protocol_test.go index b757b7e..74e1d67 100644 --- a/ui/protocol_test.go +++ b/ui/protocol_test.go @@ -42,17 +42,8 @@ func Test_ProtocolViewRender(t *testing.T) { teatest.WithDuration(time.Second*3), ) - // Send hide key - tm.Send(tea.KeyMsg{ - Type: tea.KeyRunes, - Runes: []rune("h"), - }) - - // Send quit key - tm.Send(tea.KeyMsg{ - Type: tea.KeyRunes, - Runes: []rune("ctrl+c"), - }) + // Send quit msg + tm.Send(tea.QuitMsg{}) tm.WaitFinished(t, teatest.WithFinalTimeout(time.Second)) } diff --git a/ui/status.go b/ui/status.go index a0e664e..281288b 100644 --- a/ui/status.go +++ b/ui/status.go @@ -3,6 +3,7 @@ package ui import ( "fmt" "github.com/algorandfoundation/hack-tui/internal" + "github.com/algorandfoundation/hack-tui/ui/style" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "strconv" @@ -38,15 +39,6 @@ func (m StatusViewModel) HandleMessage(msg tea.Msg) (StatusViewModel, tea.Cmd) { case tea.WindowSizeMsg: m.TerminalWidth = msg.Width m.TerminalHeight = msg.Height - // Is it a key press? - case tea.KeyMsg: - switch msg.String() { - case "ctrl+c": - return m, tea.Quit - // always hide on H press - case "h": - m.IsVisible = !m.IsVisible - } } // Return the updated model to the Bubble Tea runtime for processing. return m, nil @@ -70,30 +62,30 @@ func (m StatusViewModel) View() string { } else { size = m.TerminalWidth / 2 } - beginning := blue.Render(" Latest Round: ") + strconv.Itoa(int(m.Data.Status.LastRound)) - end := yellow.Render(strings.ToUpper(m.Data.Status.State)) + " " + beginning := style.Blue.Render(" Latest Round: ") + strconv.Itoa(int(m.Data.Status.LastRound)) + end := style.Yellow.Render(strings.ToUpper(m.Data.Status.State)) + " " middle := strings.Repeat(" ", max(0, size-(lipgloss.Width(beginning)+lipgloss.Width(end)+2))) // Last Round row1 := lipgloss.JoinHorizontal(lipgloss.Left, beginning, middle, end) - beginning = blue.Render(" Round time: ") + fmt.Sprintf("%.2fs", float64(m.Data.Metrics.RoundTime)/float64(time.Second)) - end = fmt.Sprintf("%d KB/s ", m.Data.Metrics.TX/1024) + green.Render("TX ") + beginning = style.Blue.Render(" Round time: ") + fmt.Sprintf("%.2fs", float64(m.Data.Metrics.RoundTime)/float64(time.Second)) + end = fmt.Sprintf("%d KB/s ", m.Data.Metrics.TX/1024) + style.Green.Render("TX ") middle = strings.Repeat(" ", max(0, size-(lipgloss.Width(beginning)+lipgloss.Width(end)+2))) row2 := lipgloss.JoinHorizontal(lipgloss.Left, beginning, middle, end) - beginning = blue.Render(" TPS: ") + fmt.Sprintf("%.2f", m.Data.Metrics.TPS) - end = fmt.Sprintf("%d KB/s ", m.Data.Metrics.RX/1024) + green.Render("RX ") + beginning = style.Blue.Render(" TPS: ") + fmt.Sprintf("%.2f", m.Data.Metrics.TPS) + end = fmt.Sprintf("%d KB/s ", m.Data.Metrics.RX/1024) + style.Green.Render("RX ") middle = strings.Repeat(" ", max(0, size-(lipgloss.Width(beginning)+lipgloss.Width(end)+2))) row3 := lipgloss.JoinHorizontal(lipgloss.Left, beginning, middle, end) - return WithTitle("Status", topSections(max(0, size)).Render( + return style.WithTitle("Status", style.ApplyBorder(max(0, size-2), 5, "5").Render( lipgloss.JoinVertical(lipgloss.Left, row1, "", - cyan.Render(" -- "+strconv.Itoa(m.Data.Metrics.Window)+" round average --"), + style.Cyan.Render(" -- "+strconv.Itoa(m.Data.Metrics.Window)+" round average --"), row2, row3, ))) diff --git a/ui/status_test.go b/ui/status_test.go index 53cab3f..83e77ab 100644 --- a/ui/status_test.go +++ b/ui/status_test.go @@ -2,15 +2,16 @@ package ui import ( "bytes" + "github.com/algorandfoundation/hack-tui/internal" "testing" "time" - "github.com/algorandfoundation/hack-tui/internal" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/x/exp/teatest" ) func Test_StatusViewRender(t *testing.T) { + state := internal.StateModel{ Status: internal.StatusModel{ LastRound: 1337, @@ -46,17 +47,8 @@ func Test_StatusViewRender(t *testing.T) { teatest.WithDuration(time.Second*3), ) - // Send hide key - tm.Send(tea.KeyMsg{ - Type: tea.KeyRunes, - Runes: []rune("h"), - }) - - // Send quit key - tm.Send(tea.KeyMsg{ - Type: tea.KeyRunes, - Runes: []rune("ctrl+c"), - }) + // Send quit msg + tm.Send(tea.QuitMsg{}) tm.WaitFinished(t, teatest.WithFinalTimeout(time.Second)) } diff --git a/ui/style.go b/ui/style.go deleted file mode 100644 index b0d2467..0000000 --- a/ui/style.go +++ /dev/null @@ -1,50 +0,0 @@ -package ui - -import ( - "github.com/charmbracelet/lipgloss" - "strings" -) - -var ( - rounderBorder = func() lipgloss.Style { - return lipgloss.NewStyle(). - BorderStyle(lipgloss.RoundedBorder()) - }() - topSections = func(width int) lipgloss.Style { - return rounderBorder. - Width(width - 2). - Padding(0). - Margin(0). - Height(5). - //BorderBackground(lipgloss.Color("4")). - BorderForeground(lipgloss.Color("5")) - } - - blue = func() lipgloss.Style { - return lipgloss.NewStyle().Foreground(lipgloss.Color("12")) - }() - cyan = lipgloss.NewStyle().Foreground(lipgloss.Color("14")) - yellow = lipgloss.NewStyle().Foreground(lipgloss.Color("11")) - green = lipgloss.NewStyle().Foreground(lipgloss.Color("10")) - red = lipgloss.NewStyle().Foreground(lipgloss.Color("9")) - Magenta = lipgloss.NewStyle(). - Foreground(lipgloss.Color("5")). - Render - Purple = lipgloss.NewStyle(). - Foreground(lipgloss.Color("63")). - Render - LightBlue = lipgloss.NewStyle(). - Foreground(lipgloss.Color("12")). - Render -) - -func WithTitle(title string, view string) string { - r := []rune(view) - if lipgloss.Width(view) >= len(title)+4 { - b, _, _, _, _ := rounderBorder.GetBorder() - id := strings.IndexRune(view, []rune(b.Top)[0]) - start := string(r[0:id]) - return start + title + string(r[len(title)+id:]) - } - return view -} diff --git a/ui/style/style.go b/ui/style/style.go new file mode 100644 index 0000000..1647f3f --- /dev/null +++ b/ui/style/style.go @@ -0,0 +1,75 @@ +package style + +import ( + "github.com/charmbracelet/lipgloss" + "strings" +) + +var ( + Border = func() lipgloss.Style { + return lipgloss.NewStyle(). + BorderStyle(lipgloss.RoundedBorder()) + }() + ApplyBorder = func(width int, height int, color string) lipgloss.Style { + return Border. + Width(width). + Padding(0). + Margin(0). + Height(height). + BorderForeground(lipgloss.Color(color)) + } + + Blue = func() lipgloss.Style { + return lipgloss.NewStyle().Foreground(lipgloss.Color("12")) + }() + Cyan = lipgloss.NewStyle().Foreground(lipgloss.Color("14")) + Yellow = lipgloss.NewStyle().Foreground(lipgloss.Color("11")) + Green = lipgloss.NewStyle().Foreground(lipgloss.Color("10")) + Red = lipgloss.NewStyle().Foreground(lipgloss.Color("9")) + Magenta = lipgloss.NewStyle(). + Foreground(lipgloss.Color("5")). + Render + Purple = lipgloss.NewStyle(). + Foreground(lipgloss.Color("63")). + Render + LightBlue = lipgloss.NewStyle(). + Foreground(lipgloss.Color("12")). + Render +) + +func WithTitle(title string, view string) string { + r := []rune(view) + if lipgloss.Width(view) >= len(title)+4 { + b, _, _, _, _ := Border.GetBorder() + id := strings.IndexRune(view, []rune(b.Top)[0]) + start := string(r[0:id]) + return start + title + string(r[len(title)+id:]) + } + return view +} +func WithControls(nav string, view string) string { + if nav == "" { + return view + } + controlWidth := lipgloss.Width(nav) + if lipgloss.Width(view) >= controlWidth+4 { + b, _, _, _, _ := Border.GetBorder() + find := b.BottomLeft + strings.Repeat(b.Bottom, controlWidth+4) + // TODO: allow other border colors, possibly just grab the last escape char + return strings.Replace(view, find, b.BottomLeft+strings.Repeat(b.Bottom, 4)+"\u001B[0m"+nav+"\u001B[90m", 1) + } + return view +} +func WithNavigation(controls string, view string) string { + if controls == "" { + return view + } + controlWidth := lipgloss.Width(controls) + if lipgloss.Width(view) >= controlWidth+4 { + b, _, _, _, _ := Border.GetBorder() + find := strings.Repeat(b.Bottom, controlWidth+4) + b.BottomRight + // TODO: allow other border colors, possibly just grab the last escape char + return strings.Replace(view, find, "\u001B[0m"+controls+"\u001B[90m"+strings.Repeat(b.Bottom, 4)+b.BottomRight, 1) + } + return view +} diff --git a/ui/viewport.go b/ui/viewport.go index 02f07f7..adb885f 100644 --- a/ui/viewport.go +++ b/ui/viewport.go @@ -78,6 +78,9 @@ func (m ViewportViewModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { cmds = append(cmds, cmd) switch msg := msg.(type) { + case generate.Cancel: + m.page = AccountsPage + return m, nil case error: strMsg := msg.Error() m.errorMsg = &strMsg @@ -152,7 +155,9 @@ func (m ViewportViewModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // Navigate to the transaction page return m, keys.EmitKeySelected(m.keysPage.SelectedKey()) case "ctrl+c": - return m, tea.Quit + if m.page != GeneratePage { + return m, tea.Quit + } } case tea.WindowSizeMsg: @@ -168,20 +173,18 @@ func (m ViewportViewModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } // Handle the page resize event - //switch m.page { - //case AccountsPage: m.accountsPage, cmd = m.accountsPage.HandleMessage(pageMsg) cmds = append(cmds, cmd) - //case KeysPage: + m.keysPage, cmd = m.keysPage.HandleMessage(pageMsg) cmds = append(cmds, cmd) - //case GeneratePage: + m.generatePage, cmd = m.generatePage.HandleMessage(pageMsg) cmds = append(cmds, cmd) - //case TransactionPage: + m.transactionPage, cmd = m.transactionPage.HandleMessage(pageMsg) cmds = append(cmds, cmd) - //} + m.errorPage, cmd = m.errorPage.HandleMessage(pageMsg) cmds = append(cmds, cmd) // Avoid triggering commands again @@ -198,6 +201,8 @@ func (m ViewportViewModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.generatePage, cmd = m.generatePage.HandleMessage(msg) case TransactionPage: m.transactionPage, cmd = m.transactionPage.HandleMessage(msg) + case ErrorPage: + m.errorPage, cmd = m.errorPage.HandleMessage(msg) } cmds = append(cmds, cmd) return m, tea.Batch(cmds...)