mirror of
https://github.com/sonr-io/sonr.git
synced 2026-08-02 17:31:39 +00:00
feat: add basic UI for block explorer
This commit is contained in:
+239
-54
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/huh"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/cosmos/cosmos-sdk/codec"
|
||||
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
@@ -14,77 +15,261 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const maxWidth = 100
|
||||
|
||||
var (
|
||||
red = lipgloss.AdaptiveColor{Light: "#FE5F86", Dark: "#FE5F86"}
|
||||
indigo = lipgloss.AdaptiveColor{Light: "#5A56E0", Dark: "#7571F9"}
|
||||
green = lipgloss.AdaptiveColor{Light: "#02BA84", Dark: "#02BF87"}
|
||||
)
|
||||
|
||||
type Styles struct {
|
||||
Base,
|
||||
HeaderText,
|
||||
Status,
|
||||
StatusHeader,
|
||||
Highlight,
|
||||
ErrorHeaderText,
|
||||
Help lipgloss.Style
|
||||
}
|
||||
|
||||
func NewStyles(lg *lipgloss.Renderer) *Styles {
|
||||
s := Styles{}
|
||||
s.Base = lg.NewStyle().
|
||||
Padding(1, 2, 0, 1)
|
||||
s.HeaderText = lg.NewStyle().
|
||||
Foreground(indigo).
|
||||
Bold(true).
|
||||
Padding(0, 1, 0, 1)
|
||||
s.Status = lg.NewStyle().
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(indigo).
|
||||
PaddingLeft(1).
|
||||
MarginTop(1)
|
||||
s.StatusHeader = lg.NewStyle().
|
||||
Foreground(green).
|
||||
Bold(true)
|
||||
s.Highlight = lg.NewStyle().
|
||||
Foreground(lipgloss.Color("212"))
|
||||
s.ErrorHeaderText = s.HeaderText.
|
||||
Foreground(red)
|
||||
s.Help = lg.NewStyle().
|
||||
Foreground(lipgloss.Color("240"))
|
||||
return &s
|
||||
}
|
||||
|
||||
type state int
|
||||
|
||||
const (
|
||||
statusNormal state = iota
|
||||
stateDone
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
state state
|
||||
lg *lipgloss.Renderer
|
||||
styles *Styles
|
||||
form *huh.Form
|
||||
width int
|
||||
message *tx.TxBody
|
||||
}
|
||||
|
||||
func NewModel() Model {
|
||||
return Model{
|
||||
form: huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewInput().
|
||||
Key("from").
|
||||
Title("From Address").
|
||||
Placeholder("cosmos1...").
|
||||
Validate(func(s string) error {
|
||||
if !strings.HasPrefix(s, "cosmos1") {
|
||||
return fmt.Errorf("invalid address format")
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
huh.NewInput().
|
||||
Key("to").
|
||||
Title("To Address").
|
||||
Placeholder("cosmos1...").
|
||||
Validate(func(s string) error {
|
||||
if !strings.HasPrefix(s, "cosmos1") {
|
||||
return fmt.Errorf("invalid address format")
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
huh.NewInput().
|
||||
Key("amount").
|
||||
Title("Amount").
|
||||
Placeholder("100").
|
||||
Validate(func(s string) error {
|
||||
if _, err := sdk.ParseCoinNormalized(s + "atom"); err != nil {
|
||||
return fmt.Errorf("invalid coin amount")
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
huh.NewSelect[string]().
|
||||
Key("denom").
|
||||
Title("Denom").
|
||||
Options(huh.NewOptions("atom", "osmo", "usnr", "snr")...),
|
||||
huh.NewInput().
|
||||
Key("memo").
|
||||
Title("Memo").
|
||||
Placeholder("Optional"),
|
||||
),
|
||||
m := Model{width: maxWidth}
|
||||
m.lg = lipgloss.DefaultRenderer()
|
||||
m.styles = NewStyles(m.lg)
|
||||
|
||||
m.form = huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewInput().
|
||||
Key("from").
|
||||
Title("From Address").
|
||||
Placeholder("cosmos1...").
|
||||
Validate(func(s string) error {
|
||||
if !strings.HasPrefix(s, "cosmos1") {
|
||||
return fmt.Errorf("invalid address format")
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
|
||||
huh.NewInput().
|
||||
Key("to").
|
||||
Title("To Address").
|
||||
Placeholder("cosmos1...").
|
||||
Validate(func(s string) error {
|
||||
if !strings.HasPrefix(s, "cosmos1") {
|
||||
return fmt.Errorf("invalid address format")
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
|
||||
huh.NewInput().
|
||||
Key("amount").
|
||||
Title("Amount").
|
||||
Placeholder("100").
|
||||
Validate(func(s string) error {
|
||||
if _, err := sdk.ParseCoinNormalized(s + "atom"); err != nil {
|
||||
return fmt.Errorf("invalid coin amount")
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
|
||||
huh.NewSelect[string]().
|
||||
Key("denom").
|
||||
Title("Denom").
|
||||
Options(huh.NewOptions("atom", "osmo", "usnr", "snr")...),
|
||||
|
||||
huh.NewInput().
|
||||
Key("memo").
|
||||
Title("Memo").
|
||||
Placeholder("Optional"),
|
||||
|
||||
huh.NewConfirm().
|
||||
Key("done").
|
||||
Title("Ready to convert?").
|
||||
Validate(func(v bool) error {
|
||||
if !v {
|
||||
return fmt.Errorf("Please confirm when you're ready to convert")
|
||||
}
|
||||
return nil
|
||||
}).
|
||||
Affirmative("Yes, convert!").
|
||||
Negative("Not yet"),
|
||||
),
|
||||
}
|
||||
).
|
||||
WithWidth(60).
|
||||
WithShowHelp(false).
|
||||
WithShowErrors(false)
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
func (m Model) Init() tea.Cmd {
|
||||
return m.form.Init()
|
||||
}
|
||||
|
||||
func min(x, y int) int {
|
||||
if x > y {
|
||||
return y
|
||||
}
|
||||
return x
|
||||
}
|
||||
|
||||
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
m.width = min(msg.Width, maxWidth) - m.styles.Base.GetHorizontalFrameSize()
|
||||
case tea.KeyMsg:
|
||||
switch msg.String() {
|
||||
case "esc", "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
}
|
||||
}
|
||||
|
||||
var cmds []tea.Cmd
|
||||
|
||||
form, cmd := m.form.Update(msg)
|
||||
if f, ok := form.(*huh.Form); ok {
|
||||
m.form = f
|
||||
if m.form.State == huh.StateCompleted {
|
||||
m.buildMessage()
|
||||
}
|
||||
cmds = append(cmds, cmd)
|
||||
}
|
||||
return m, cmd
|
||||
|
||||
if m.form.State == huh.StateCompleted {
|
||||
m.buildMessage()
|
||||
cmds = append(cmds, tea.Quit)
|
||||
}
|
||||
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
func (m Model) View() string {
|
||||
formView := m.form.View()
|
||||
messageView := m.getMessageView()
|
||||
return fmt.Sprintf("%s\n\n%s", formView, messageView)
|
||||
s := m.styles
|
||||
|
||||
switch m.form.State {
|
||||
case huh.StateCompleted:
|
||||
pklCode := m.generatePkl()
|
||||
messageView := m.getMessageView()
|
||||
var b strings.Builder
|
||||
fmt.Fprintf(&b, "Final Tx:\n\n%s\n\n%s", pklCode, messageView)
|
||||
return s.Status.Margin(0, 1).Padding(1, 2).Width(80).Render(b.String()) + "\n\n"
|
||||
default:
|
||||
var schemaType string
|
||||
if m.form.GetString("schemaType") != "" {
|
||||
schemaType = "Schema Type: " + m.form.GetString("schemaType")
|
||||
}
|
||||
|
||||
v := strings.TrimSuffix(m.form.View(), "\n\n")
|
||||
form := m.lg.NewStyle().Margin(1, 0).Render(v)
|
||||
|
||||
var status string
|
||||
{
|
||||
preview := "(Preview will appear here)"
|
||||
if m.form.GetString("schema") != "" {
|
||||
preview = m.generatePkl()
|
||||
}
|
||||
|
||||
const statusWidth = 40
|
||||
statusMarginLeft := m.width - statusWidth - lipgloss.Width(form) - s.Status.GetMarginRight()
|
||||
status = s.Status.
|
||||
Height(lipgloss.Height(form)).
|
||||
Width(statusWidth).
|
||||
MarginLeft(statusMarginLeft).
|
||||
Render(s.StatusHeader.Render("Pkl Preview") + "\n" +
|
||||
schemaType + "\n\n" +
|
||||
preview)
|
||||
}
|
||||
|
||||
errors := m.form.Errors()
|
||||
header := m.appBoundaryView("Sonr TX Builder")
|
||||
if len(errors) > 0 {
|
||||
header = m.appErrorBoundaryView(m.errorView())
|
||||
}
|
||||
body := lipgloss.JoinHorizontal(lipgloss.Top, form, status)
|
||||
|
||||
footer := m.appBoundaryView(m.form.Help().ShortHelpView(m.form.KeyBinds()))
|
||||
if len(errors) > 0 {
|
||||
footer = m.appErrorBoundaryView("")
|
||||
}
|
||||
|
||||
return s.Base.Render(header + "\n" + body + "\n\n" + footer)
|
||||
}
|
||||
}
|
||||
|
||||
func (m Model) errorView() string {
|
||||
var s string
|
||||
for _, err := range m.form.Errors() {
|
||||
s += err.Error()
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (m Model) appBoundaryView(text string) string {
|
||||
return lipgloss.PlaceHorizontal(
|
||||
m.width,
|
||||
lipgloss.Left,
|
||||
m.styles.HeaderText.Render(text),
|
||||
lipgloss.WithWhitespaceChars("="),
|
||||
lipgloss.WithWhitespaceForeground(indigo),
|
||||
)
|
||||
}
|
||||
|
||||
func (m Model) appErrorBoundaryView(text string) string {
|
||||
return lipgloss.PlaceHorizontal(
|
||||
m.width,
|
||||
lipgloss.Left,
|
||||
m.styles.ErrorHeaderText.Render(text),
|
||||
lipgloss.WithWhitespaceChars("="),
|
||||
lipgloss.WithWhitespaceForeground(red),
|
||||
)
|
||||
}
|
||||
|
||||
func (m Model) generatePkl() string {
|
||||
schemaType := m.form.GetString("schemaType")
|
||||
schema := m.form.GetString("schema")
|
||||
|
||||
// This is a placeholder for the actual conversion logic
|
||||
// In a real implementation, you would parse the schema and generate Pkl code
|
||||
return fmt.Sprintf("// Converted from %s\n\nclass ConvertedSchema {\n // TODO: Implement conversion from %s\n // Original schema:\n /*\n%s\n */\n}", schemaType, schemaType, schema)
|
||||
}
|
||||
|
||||
func (m *Model) buildMessage() {
|
||||
@@ -120,7 +305,7 @@ func (m Model) getMessageView() string {
|
||||
return fmt.Sprintf("Current Message:\n%s", string(jsonBytes))
|
||||
}
|
||||
|
||||
func runTUIForm() (*tx.TxBody, error) {
|
||||
func RunTUIForm() (*tx.TxBody, error) {
|
||||
m := NewModel()
|
||||
p := tea.NewProgram(m)
|
||||
|
||||
@@ -139,10 +324,10 @@ func runTUIForm() (*tx.TxBody, error) {
|
||||
|
||||
func NewBuildProtoMsgCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "build-proto-msg",
|
||||
Short: "Build a Cosmos SDK protobuf message using a TUI form",
|
||||
Use: "dash",
|
||||
Short: "TUI for managing the local Sonr validator node",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
txBody, err := runTUIForm()
|
||||
txBody, err := RunTUIForm()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user