diff --git a/client/cmd/bisonw-desktop/go.mod b/client/cmd/bisonw-desktop/go.mod index 83f2469405..8e70bb332f 100644 --- a/client/cmd/bisonw-desktop/go.mod +++ b/client/cmd/bisonw-desktop/go.mod @@ -19,7 +19,7 @@ require ( github.com/athanorlabs/go-dleq v0.1.0 // indirect github.com/bisoncraft/go-monero v0.1.1 // indirect github.com/bisoncraft/op-geth v0.0.0-20250729074358-3cfe4f15e91c // indirect - github.com/bits-and-blooms/bitset v1.20.0 // indirect + github.com/bits-and-blooms/bitset v1.22.0 // indirect github.com/consensys/gnark-crypto v0.18.0 // indirect github.com/crate-crypto/go-eth-kzg v1.4.0 // indirect github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a // indirect @@ -159,7 +159,7 @@ require ( github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/progrium/darwinkit v0.5.0 - github.com/rivo/uniseg v0.2.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect github.com/tevino/abool v1.2.0 // indirect diff --git a/client/cmd/bisonw-desktop/go.sum b/client/cmd/bisonw-desktop/go.sum index f2704bf987..9dfe9dea6f 100644 --- a/client/cmd/bisonw-desktop/go.sum +++ b/client/cmd/bisonw-desktop/go.sum @@ -126,8 +126,8 @@ github.com/bisoncraft/op-geth v0.0.0-20250729074358-3cfe4f15e91c h1:cmK4HxTpEutY github.com/bisoncraft/op-geth v0.0.0-20250729074358-3cfe4f15e91c/go.mod h1:AVdw4MgxUWZ0mhB+VxKOfXH2Z6WJA6rX6YrYrY2bii0= github.com/bisoncraft/webview_go v0.1.0 h1:F0ZiJSYzDqE4HJhI1u5I+Y7H51bYzprDum0tAtMnOw4= github.com/bisoncraft/webview_go v0.1.0/go.mod h1:cDmD2SZRZJl3wXKsgU3cRLA64HCcJL9Kxa+Hp5u4so4= -github.com/bits-and-blooms/bitset v1.20.0 h1:2F+rfL86jE2d/bmw7OhqUg2Sj/1rURkBn3MdfoPyRVU= -github.com/bits-and-blooms/bitset v1.20.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/bits-and-blooms/bitset v1.22.0 h1:Tquv9S8+SGaS3EhyA+up3FXzmkhxPGjQQCkcs2uw7w4= +github.com/bits-and-blooms/bitset v1.22.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/bkielbasa/cyclop v1.2.0/go.mod h1:qOI0yy6A7dYC4Zgsa72Ppm9kONl0RoIlPbzot9mhmeI= github.com/bluele/gcache v0.0.2/go.mod h1:m15KV+ECjptwSPxKhOhQoAFQVtUFjTVkc3H8o0t/fp0= @@ -1022,8 +1022,9 @@ github.com/quasilyte/regex/syntax v0.0.0-20200407221936-30656e2c4a95/go.mod h1:r github.com/quasilyte/regex/syntax v0.0.0-20200805063351-8f842688393c/go.mod h1:rlzQ04UMyJXu/aOvhd8qT+hvDrFpiwqp8MRXDY9szc0= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= diff --git a/dex/lexi/cmd/lexidbexplorer/db.go b/dex/lexi/cmd/lexidbexplorer/db.go new file mode 100644 index 0000000000..c34d46e877 --- /dev/null +++ b/dex/lexi/cmd/lexidbexplorer/db.go @@ -0,0 +1,224 @@ +// This code is available on the terms of the project LICENSE.md file, +// also available online at https://blueoakcouncil.org/license/1.0.0. + +package main + +import ( + "errors" + "fmt" + "sort" + "strings" + + "decred.org/dcrdex/dex/lexi" + "github.com/dgraph-io/badger" +) + +// These constants mirror unexported values from the lexi package. +// They are duplicated here because they are internal implementation details +// that shouldn't be exported just for the explorer tool. +const prefixSize = 2 // lexi.prefixSize + +// prefixToNamePrefix mirrors lexi.prefixToNamePrefix - the prefix used to +// map table/index prefixes to their names. +var prefixToNamePrefix = [prefixSize]byte{0x00, 0x00} + +// nameToPrefixPrefix mirrors lexi.nameToPrefixPrefix - the prefix used to map +// table/index names to their key prefixes. +var nameToPrefixPrefix = [prefixSize]byte{0x00, 0x01} + +// idToKeyPrefix mirrors lexi.idToKeyPrefix - the prefix used to map DBID -> key. +var idToKeyPrefix = [prefixSize]byte{0x00, 0x04} + +const indexNameSeparator = "__idx__" + +// entryData holds the key, optional index key, and value for a database entry. +type entryData struct { + key []byte + indexKey []byte // only set when iterating an index + value []byte +} + +func prefixedKey(prefix [prefixSize]byte, k []byte) []byte { + pk := make([]byte, prefixSize+len(k)) + copy(pk, prefix[:]) + copy(pk[prefixSize:], k) + return pk +} + +func cloneBytes(b []byte) []byte { + if len(b) == 0 { + return nil + } + c := make([]byte, len(b)) + copy(c, b) + return c +} + +func prefixForName(bdb *badger.DB, name string) ([prefixSize]byte, error) { + var prefix [prefixSize]byte + err := bdb.View(func(txn *badger.Txn) error { + it, err := txn.Get(prefixedKey(nameToPrefixPrefix, []byte(name))) + if err != nil { + return err + } + return it.Value(func(b []byte) error { + if len(b) != prefixSize { + return fmt.Errorf("unexpected prefix size %d for name %q", len(b), name) + } + copy(prefix[:], b) + return nil + }) + }) + return prefix, err +} + +// listTablesAndIndexes iterates the prefixToNamePrefix to get all table and +// index names, returning a map of tableName -> []indexNames. +func listTablesAndIndexes(bdb *badger.DB) (map[string][]string, error) { + tables := make(map[string][]string) // tableName -> []indexNames + + err := bdb.View(func(txn *badger.Txn) error { + opts := badger.DefaultIteratorOptions + opts.Prefix = prefixToNamePrefix[:] + it := txn.NewIterator(opts) + defer it.Close() + + for it.Rewind(); it.Valid(); it.Next() { + item := it.Item() + err := item.Value(func(nameB []byte) error { + name := string(nameB) + if strings.Contains(name, indexNameSeparator) { + // It's an index: "tableName__idx__indexName" + parts := strings.SplitN(name, indexNameSeparator, 2) + if len(parts) == 2 { + tableName, indexName := parts[0], parts[1] + tables[tableName] = append(tables[tableName], indexName) + } + } else { + // It's a table + if _, exists := tables[name]; !exists { + tables[name] = []string{} + } + } + return nil + }) + if err != nil { + return err + } + } + return nil + }) + + // Sort index names for consistent display + for _, indexes := range tables { + sort.Strings(indexes) + } + + return tables, err +} + +// iterateTable iterates all entries in a table and returns the entries. +func iterateTable(db *lexi.DB, tableName string, limit int) ([]entryData, error) { + table, err := db.Table(tableName) + if err != nil { + return nil, err + } + + var entries []entryData + err = table.Iterate(nil, func(it *lexi.Iter) error { + k, err := it.K() + if err != nil { + return err + } + + var v []byte + if err := it.V(func(vB []byte) error { + v = cloneBytes(vB) + return nil + }); err != nil { + return err + } + + entries = append(entries, entryData{ + key: cloneBytes(k), + value: v, + }) + + if limit > 0 && len(entries) >= limit { + return lexi.ErrEndIteration + } + return nil + }) + + return entries, err +} + +// iterateIndex iterates all entries in an index and returns the entries +// in index order. +func iterateIndex(bdb *badger.DB, db *lexi.DB, tableName, indexName string, limit int) ([]entryData, error) { + table, err := db.Table(tableName) + if err != nil { + return nil, err + } + + indexPrefix, err := prefixForName(bdb, tableName+indexNameSeparator+indexName) + if errors.Is(err, badger.ErrKeyNotFound) { + return nil, fmt.Errorf("index %q not found on table %q", indexName, tableName) + } + if err != nil { + return nil, err + } + + var entries []entryData + err = bdb.View(func(txn *badger.Txn) error { + opts := badger.DefaultIteratorOptions + opts.Prefix = indexPrefix[:] + it := txn.NewIterator(opts) + defer it.Close() + + for it.Rewind(); it.Valid(); it.Next() { + item := it.Item() + idxEntryKey := item.KeyCopy(nil) + if len(idxEntryKey) < prefixSize+8 { + // indexPrefix + (idxKey + dbid) must be at least 8 bytes beyond prefix + continue + } + + rest := idxEntryKey[prefixSize:] + if len(rest) < 8 { + continue + } + idxKey := cloneBytes(rest[:len(rest)-8]) + dbIDB := rest[len(rest)-8:] + + // Resolve the original table key from DBID. + keyItem, err := txn.Get(prefixedKey(idToKeyPrefix, dbIDB)) + if err != nil { + return err + } + keyB, err := keyItem.ValueCopy(nil) + if err != nil { + return err + } + + // Resolve the value via the table API (decodes the internal datum). + vB, err := table.GetRaw(keyB, lexi.WithGetTxn(txn)) + if err != nil { + return err + } + + entries = append(entries, entryData{ + key: cloneBytes(keyB), + indexKey: idxKey, + value: cloneBytes(vB), + }) + + if limit > 0 && len(entries) >= limit { + break + } + } + return nil + }) + + return entries, err +} diff --git a/dex/lexi/cmd/lexidbexplorer/display.go b/dex/lexi/cmd/lexidbexplorer/display.go new file mode 100644 index 0000000000..bea0238892 --- /dev/null +++ b/dex/lexi/cmd/lexidbexplorer/display.go @@ -0,0 +1,154 @@ +// This code is available on the terms of the project LICENSE.md file, +// also available online at https://blueoakcouncil.org/license/1.0.0. + +package main + +import ( + "encoding/hex" + "encoding/json" + "fmt" + "strings" + "unicode" + "unicode/utf8" +) + +// truncate shortens a string to maxLen, adding "..." if truncated. +func truncate(s string, maxLen int) string { + if maxLen <= 0 { + return "" + } + r := []rune(s) + if len(r) <= maxLen { + return s + } + if maxLen <= 3 { + return string(r[:maxLen]) + } + return string(r[:maxLen-3]) + "..." +} + +// tryParseJSON attempts to parse raw bytes as JSON. +// Returns the parsed data and true if successful, nil and false otherwise. +func tryParseJSON(raw []byte) (any, bool) { + var data any + if err := json.Unmarshal(raw, &data); err == nil { + return data, true + } + return nil, false +} + +// isPrintableString returns true if raw is valid UTF-8 with only printable characters. +func isPrintableString(raw []byte) bool { + return utf8.Valid(raw) && isPrintable(raw) +} + +// formatValue attempts to display raw bytes in the most human-readable format. +// It tries JSON first, then UTF-8 string, then falls back to hex dump. +func formatValue(raw []byte) string { + if len(raw) == 0 { + return "" + } + + // Try JSON (most common encoding) + if data, ok := tryParseJSON(raw); ok { + if pretty, err := json.MarshalIndent(data, "", " "); err == nil { + return string(pretty) + } + } + + // Try UTF-8 string (if printable) + if isPrintableString(raw) { + return string(raw) + } + + // Fall back to hex dump with ASCII sidebar + return hexDump(raw) +} + +// formatKey formats a key for display. +func formatKey(raw []byte) string { + if len(raw) == 0 { + return "" + } + + if isPrintableString(raw) { + return string(raw) + } + return hex.EncodeToString(raw) +} + +// formatKeyShort returns a shortened key representation for list display. +func formatKeyShort(raw []byte, maxLen int) string { + return truncate(formatKey(raw), maxLen) +} + +// formatValuePreview returns a short preview of the value for list display. +func formatValuePreview(raw []byte, maxLen int) string { + if len(raw) == 0 { + return "" + } + + // Try JSON (compact form for preview) + if data, ok := tryParseJSON(raw); ok { + if compact, err := json.Marshal(data); err == nil { + return truncate(string(compact), maxLen) + } + } + + // Try string + if isPrintableString(raw) { + return truncate(string(raw), maxLen) + } + + // Hex + return truncate(hex.EncodeToString(raw), maxLen) +} + +// isPrintable returns true if all runes in the byte slice are printable. +func isPrintable(b []byte) bool { + for len(b) > 0 { + r, size := utf8.DecodeRune(b) + if r == utf8.RuneError && size == 1 { + return false + } + if !unicode.IsPrint(r) && !unicode.IsSpace(r) { + return false + } + b = b[size:] + } + return true +} + +// hexDump creates a hex dump with ASCII sidebar, similar to hexdump -C. +func hexDump(data []byte) string { + var sb strings.Builder + for i := 0; i < len(data); i += 16 { + // Offset + sb.WriteString(fmt.Sprintf("%08x ", i)) + + // Hex bytes + for j := 0; j < 16; j++ { + if i+j < len(data) { + sb.WriteString(fmt.Sprintf("%02x ", data[i+j])) + } else { + sb.WriteString(" ") + } + if j == 7 { + sb.WriteString(" ") + } + } + + // ASCII sidebar + sb.WriteString(" |") + for j := 0; j < 16 && i+j < len(data); j++ { + c := data[i+j] + if c >= 32 && c < 127 { + sb.WriteByte(c) + } else { + sb.WriteByte('.') + } + } + sb.WriteString("|\n") + } + return strings.TrimSuffix(sb.String(), "\n") +} diff --git a/dex/lexi/cmd/lexidbexplorer/main.go b/dex/lexi/cmd/lexidbexplorer/main.go new file mode 100644 index 0000000000..b5144a6d37 --- /dev/null +++ b/dex/lexi/cmd/lexidbexplorer/main.go @@ -0,0 +1,61 @@ +// This code is available on the terms of the project LICENSE.md file, +// also available online at https://blueoakcouncil.org/license/1.0.0. + +// lexidbexplorer is an interactive CLI tool for exploring lexi databases. +// It provides arrow-key navigation through tables, indexes, and entries +// with human-readable display of values. +// +// Usage: +// +// go run ./dex/lexi/cmd/lexidbexplorer /path/to/lexi.db +package main + +import ( + "fmt" + "os" + + "decred.org/dcrdex/dex" + "decred.org/dcrdex/dex/lexi" + tea "github.com/charmbracelet/bubbletea" +) + +func main() { + if len(os.Args) < 2 { + fmt.Fprintln(os.Stderr, "Usage: lexidbexplorer ") + os.Exit(1) + } + + dbPath := os.Args[1] + + // Check if path exists + if _, err := os.Stat(dbPath); os.IsNotExist(err) { + fmt.Fprintf(os.Stderr, "Error: database path does not exist: %s\n", dbPath) + os.Exit(1) + } + + // Open the lexi database + log := dex.StdOutLogger("LEXI", dex.LevelOff) // Suppress logging + db, err := lexi.New(&lexi.Config{ + Path: dbPath, + Log: log, + }) + if err != nil { + fmt.Fprintf(os.Stderr, "Error opening database: %v\n", err) + os.Exit(1) + } + defer db.Close() + + // Initialize the model + m, err := initModel(db, db.DB) + if err != nil { + fmt.Fprintf(os.Stderr, "Error initializing explorer: %v\n", err) + os.Exit(1) + } + + // Run the bubbletea program + p := tea.NewProgram(m, tea.WithAltScreen()) + if _, err := p.Run(); err != nil { + fmt.Fprintf(os.Stderr, "Error running explorer: %v\n", err) + os.Exit(1) + } +} diff --git a/dex/lexi/cmd/lexidbexplorer/model.go b/dex/lexi/cmd/lexidbexplorer/model.go new file mode 100644 index 0000000000..af6663396d --- /dev/null +++ b/dex/lexi/cmd/lexidbexplorer/model.go @@ -0,0 +1,391 @@ +// This code is available on the terms of the project LICENSE.md file, +// also available online at https://blueoakcouncil.org/license/1.0.0. + +package main + +import ( + "sort" + + "decred.org/dcrdex/dex/lexi" + tea "github.com/charmbracelet/bubbletea" + "github.com/dgraph-io/badger" +) + +// viewState represents the current view in the application. +type viewState int + +const ( + viewTables viewState = iota // List of tables + viewTableDetail // Table details (indexes + view all) + viewEntries // Iterating entries + viewEntryDetail // Viewing a single entry in detail +) + +const ( + maxKeyDisplay = 40 // Max characters for key in list + maxValuePreview = 60 // Max characters for value preview + + // UI layout constants + entriesViewOverhead = 8 // Lines used by header/footer in entries view + detailViewOverhead = 6 // Lines used by header/footer in detail view + minVisibleLines = 5 // Minimum visible content lines + minDetailVisibleLines = 10 // Minimum visible lines in detail view +) + +// model is the bubbletea model for the DB explorer. +type model struct { + db *lexi.DB + bdb *badger.DB + tables map[string][]string // tableName -> indexNames + tableNames []string // sorted table names + + state viewState + cursor int + + // Current selection context + currentTable string + currentIndex string // empty = iterate table directly + + // Entry viewing + entries []entryData + entriesOffset int + selectedEntry int + loading bool + + // Entry detail scrolling + detailScrollOffset int + detailTotalLines int + detailLines []string + + // UI dimensions + width, height int + + // Error message + err error +} + +var _ tea.Model = (*model)(nil) + +// visibleEntriesLines returns the number of visible lines for the entries list. +func (m model) visibleEntriesLines() int { + return max(minVisibleLines, m.height-entriesViewOverhead) +} + +// visibleDetailLines returns the number of visible lines for entry detail view. +func (m model) visibleDetailLines() int { + return max(minDetailVisibleLines, m.height-detailViewOverhead) +} + +// maxDetailScroll returns the maximum scroll offset for the detail view. +func (m model) maxDetailScroll() int { + return max(0, m.detailTotalLines-m.visibleDetailLines()) +} + +// Init initializes the model. +func (m model) Init() tea.Cmd { + return nil +} + +// initModel creates a new model with the given database. +func initModel(db *lexi.DB, bdb *badger.DB) (model, error) { + tables, err := listTablesAndIndexes(bdb) + if err != nil { + return model{}, err + } + + // Sort table names for consistent display + tableNames := make([]string, 0, len(tables)) + for name := range tables { + tableNames = append(tableNames, name) + } + sort.Strings(tableNames) + + return model{ + db: db, + bdb: bdb, + tables: tables, + tableNames: tableNames, + state: viewTables, + width: 80, + height: 24, + }, nil +} + +// Update handles messages and updates the model. +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyMsg: + return m.handleKeyPress(msg) + case tea.WindowSizeMsg: + m.width = msg.Width + m.height = msg.Height + return m, nil + case entriesLoadedMsg: + m.loading = false + if msg.err != nil { + m.err = msg.err + return m, nil + } + m.entries = msg.entries + return m, nil + } + return m, nil +} + +// entriesLoadedMsg is sent when entries are loaded from the database. +type entriesLoadedMsg struct { + entries []entryData + err error +} + +// handleKeyPress processes keyboard input. +func (m model) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) { + switch msg.String() { + case "ctrl+c", "q": + return m, tea.Quit + + case "up", "k": + return m.moveCursorUp() + + case "down", "j": + return m.moveCursorDown() + + case "enter", " ": + return m.selectItem() + + case "esc", "backspace": + return m.goBack() + + case "home", "g": + m.cursor = 0 + m.entriesOffset = 0 + m.detailScrollOffset = 0 + return m, nil + + case "end", "G": + return m.goToEnd() + + case "pgup", "ctrl+u": + return m.pageUp() + + case "pgdown", "ctrl+d": + return m.pageDown() + } + return m, nil +} + +// moveCursorUp moves the cursor up. +func (m model) moveCursorUp() (tea.Model, tea.Cmd) { + if m.state == viewEntryDetail { + if m.detailScrollOffset > 0 { + m.detailScrollOffset-- + } + return m, nil + } + if m.cursor > 0 { + m.cursor-- + if m.state == viewEntries && m.cursor < m.entriesOffset { + m.entriesOffset = m.cursor + } + } + return m, nil +} + +// moveCursorDown moves the cursor down. +func (m model) moveCursorDown() (tea.Model, tea.Cmd) { + if m.state == viewEntryDetail { + if m.detailScrollOffset < m.maxDetailScroll() { + m.detailScrollOffset++ + } + return m, nil + } + maxCursor := m.getMaxCursor() + if m.cursor < maxCursor { + m.cursor++ + visibleLines := m.visibleEntriesLines() + if m.state == viewEntries && m.cursor >= m.entriesOffset+visibleLines { + m.entriesOffset = m.cursor - visibleLines + 1 + } + } + return m, nil +} + +// goToEnd moves cursor to the end of the current list. +func (m model) goToEnd() (tea.Model, tea.Cmd) { + if m.state == viewEntryDetail { + m.detailScrollOffset = m.maxDetailScroll() + return m, nil + } + m.cursor = m.getMaxCursor() + if m.state == viewEntries { + visibleLines := m.visibleEntriesLines() + if m.cursor >= visibleLines { + m.entriesOffset = m.cursor - visibleLines + 1 + } + } + return m, nil +} + +// pageUp scrolls up by a page. +func (m model) pageUp() (tea.Model, tea.Cmd) { + if m.state == viewEntryDetail { + m.detailScrollOffset = max(0, m.detailScrollOffset-m.visibleDetailLines()) + return m, nil + } + if m.state == viewEntries { + m.cursor = max(0, m.cursor-m.visibleEntriesLines()) + m.entriesOffset = m.cursor + } + return m, nil +} + +// pageDown scrolls down by a page. +func (m model) pageDown() (tea.Model, tea.Cmd) { + if m.state == viewEntryDetail { + m.detailScrollOffset = min(m.maxDetailScroll(), m.detailScrollOffset+m.visibleDetailLines()) + return m, nil + } + if m.state == viewEntries { + visibleLines := m.visibleEntriesLines() + m.cursor = min(m.getMaxCursor(), m.cursor+visibleLines) + if m.cursor >= m.entriesOffset+visibleLines { + m.entriesOffset = m.cursor - visibleLines + 1 + } + } + return m, nil +} + +// getMaxCursor returns the maximum valid cursor position. +func (m model) getMaxCursor() int { + switch m.state { + case viewTables: + return max(0, len(m.tableNames)-1) + case viewTableDetail: + // "View all entries" + indexes + return len(m.tables[m.currentTable]) + case viewEntries: + return max(0, len(m.entries)-1) + case viewEntryDetail: + return 0 + } + return 0 +} + +// selectItem selects the current item based on state. +func (m model) selectItem() (tea.Model, tea.Cmd) { + switch m.state { + case viewTables: + if len(m.tableNames) == 0 { + return m, nil + } + m.currentTable = m.tableNames[m.cursor] + m.state = viewTableDetail + m.cursor = 0 + return m, nil + + case viewTableDetail: + indexes := m.tables[m.currentTable] + if m.cursor == 0 { + // "View all entries" selected + m.currentIndex = "" + } else { + // An index was selected + m.currentIndex = indexes[m.cursor-1] + } + m.state = viewEntries + m.entries = nil + m.entriesOffset = 0 + m.cursor = 0 + m.loading = true + return m, m.loadEntries() + + case viewEntries: + if len(m.entries) == 0 { + return m, nil + } + m.selectedEntry = m.cursor + m.state = viewEntryDetail + m.detailScrollOffset = 0 + // Cache the rendered lines so line-count and rendering never drift. + entry := m.entries[m.cursor] + m.detailLines = buildEntryDetailLines(entry) + m.detailTotalLines = len(m.detailLines) + return m, nil + + case viewEntryDetail: + // Press enter to go back from detail view + m.state = viewEntries + m.cursor = m.selectedEntry + m.detailLines = nil + m.detailTotalLines = 0 + return m, nil + } + return m, nil +} + +// goBack returns to the previous view. +func (m model) goBack() (tea.Model, tea.Cmd) { + switch m.state { + case viewTables: + return m, tea.Quit + case viewTableDetail: + m.state = viewTables + m.cursor = 0 + for i, name := range m.tableNames { + if name == m.currentTable { + m.cursor = i + break + } + } + m.currentTable = "" + return m, nil + case viewEntries: + m.state = viewTableDetail + m.cursor = 0 + m.entries = nil + m.currentIndex = "" + return m, nil + case viewEntryDetail: + m.state = viewEntries + m.cursor = m.selectedEntry + m.detailLines = nil + m.detailTotalLines = 0 + return m, nil + } + return m, nil +} + +// loadEntries returns a command to load entries from the database. +// We load all entries at once (limit=0) since lexi doesn't have built-in +// pagination with seek. +func (m model) loadEntries() tea.Cmd { + return func() tea.Msg { + var entries []entryData + var err error + if m.currentIndex == "" { + entries, err = iterateTable(m.db, m.currentTable, 0) // 0 = no limit + } else { + entries, err = iterateIndex(m.bdb, m.db, m.currentTable, m.currentIndex, 0) // 0 = no limit + } + return entriesLoadedMsg{entries: entries, err: err} + } +} + +// View renders the current view. +func (m model) View() string { + if m.err != nil { + return renderError(m) + } + + switch m.state { + case viewTables: + return renderTablesView(m) + case viewTableDetail: + return renderTableDetailView(m) + case viewEntries: + return renderEntriesView(m) + case viewEntryDetail: + return renderEntryDetailView(m) + } + return "" +} diff --git a/dex/lexi/cmd/lexidbexplorer/views.go b/dex/lexi/cmd/lexidbexplorer/views.go new file mode 100644 index 0000000000..6a96fe866c --- /dev/null +++ b/dex/lexi/cmd/lexidbexplorer/views.go @@ -0,0 +1,279 @@ +// This code is available on the terms of the project LICENSE.md file, +// also available online at https://blueoakcouncil.org/license/1.0.0. + +package main + +import ( + "fmt" + "strings" + + "github.com/charmbracelet/lipgloss" +) + +var ( + // Styles + titleStyle = lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("205")). + MarginBottom(1) + + selectedStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("229")). + Background(lipgloss.Color("57")). + Bold(true) + + normalStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("252")) + + dimStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("240")) + + headerStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("39")). + Bold(true) + + errorStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("196")). + Bold(true) + + helpStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("241")). + MarginTop(1) + + keyStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("39")) + + valueStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("252")) + + indexKeyStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("208")) +) + +func buildEntryDetailLines(entry entryData) []string { + var lines []string + + // Key section + lines = append(lines, headerStyle.Render("Key:")) + for _, line := range strings.Split(formatKey(entry.key), "\n") { + lines = append(lines, keyStyle.Render(line)) + } + lines = append(lines, "") + + // Index key section (if present) + if len(entry.indexKey) > 0 { + lines = append(lines, headerStyle.Render("Index Key:")) + for _, line := range strings.Split(formatKey(entry.indexKey), "\n") { + lines = append(lines, indexKeyStyle.Render(line)) + } + lines = append(lines, "") + } + + // Value section + lines = append(lines, headerStyle.Render("Value:")) + for _, line := range strings.Split(formatValue(entry.value), "\n") { + lines = append(lines, valueStyle.Render(line)) + } + + return lines +} + +// renderError renders an error message. +func renderError(m model) string { + var sb strings.Builder + sb.WriteString(titleStyle.Render("Lexi DB Explorer")) + sb.WriteString("\n\n") + sb.WriteString(errorStyle.Render(fmt.Sprintf("Error: %v", m.err))) + sb.WriteString("\n\n") + sb.WriteString(helpStyle.Render("Press q to quit")) + return sb.String() +} + +// renderTablesView renders the list of tables. +func renderTablesView(m model) string { + var sb strings.Builder + + sb.WriteString(titleStyle.Render("Lexi DB Explorer - Tables")) + sb.WriteString("\n\n") + + if len(m.tableNames) == 0 { + sb.WriteString(dimStyle.Render("No tables found")) + } else { + for i, name := range m.tableNames { + indexCount := len(m.tables[name]) + indexInfo := "" + if indexCount > 0 { + indexInfo = dimStyle.Render(fmt.Sprintf(" (%d indexes)", indexCount)) + } + + line := fmt.Sprintf(" %s%s", name, indexInfo) + if i == m.cursor { + sb.WriteString(selectedStyle.Render(fmt.Sprintf("> %s", name))) + sb.WriteString(indexInfo) + } else { + sb.WriteString(normalStyle.Render(line)) + } + sb.WriteString("\n") + } + } + + sb.WriteString("\n") + sb.WriteString(helpStyle.Render("↑/↓: navigate • enter: select • q: quit")) + + return sb.String() +} + +// renderTableDetailView renders the table detail view with indexes. +func renderTableDetailView(m model) string { + var sb strings.Builder + + sb.WriteString(titleStyle.Render(fmt.Sprintf("Table: %s", m.currentTable))) + sb.WriteString("\n\n") + + indexes := m.tables[m.currentTable] + + // First item: View all entries + viewAllLine := "View all entries" + if m.cursor == 0 { + sb.WriteString(selectedStyle.Render(fmt.Sprintf("> %s", viewAllLine))) + } else { + sb.WriteString(normalStyle.Render(fmt.Sprintf(" %s", viewAllLine))) + } + sb.WriteString("\n") + + // List indexes + if len(indexes) > 0 { + sb.WriteString("\n") + sb.WriteString(headerStyle.Render("Indexes:")) + sb.WriteString("\n") + for i, idxName := range indexes { + if i+1 == m.cursor { + sb.WriteString(selectedStyle.Render(fmt.Sprintf("> %s", idxName))) + } else { + sb.WriteString(normalStyle.Render(fmt.Sprintf(" %s", idxName))) + } + sb.WriteString("\n") + } + } + + sb.WriteString("\n") + sb.WriteString(helpStyle.Render("↑/↓: navigate • enter: select • esc: back • q: quit")) + + return sb.String() +} + +// renderEntriesView renders the list of entries. +func renderEntriesView(m model) string { + var sb strings.Builder + + // Header + title := fmt.Sprintf("Table: %s", m.currentTable) + if m.currentIndex != "" { + title += fmt.Sprintf(" (Index: %s)", m.currentIndex) + } + sb.WriteString(titleStyle.Render(title)) + sb.WriteString("\n") + + entryCount := fmt.Sprintf("%d entries", len(m.entries)) + if m.loading { + entryCount += " (loading...)" + } + sb.WriteString(dimStyle.Render(entryCount)) + sb.WriteString("\n\n") + + if len(m.entries) == 0 { + if m.loading { + sb.WriteString(dimStyle.Render("Loading entries...")) + } else { + sb.WriteString(dimStyle.Render("No entries found")) + } + } else { + // Calculate visible range + visibleLines := m.visibleEntriesLines() + start := m.entriesOffset + end := min(start+visibleLines, len(m.entries)) + + for i := start; i < end; i++ { + entry := m.entries[i] + keyStr := formatKeyShort(entry.key, maxKeyDisplay) + valueStr := formatValuePreview(entry.value, maxValuePreview) + + var line string + if m.currentIndex != "" && len(entry.indexKey) > 0 { + idxKeyStr := formatKeyShort(entry.indexKey, 20) + line = fmt.Sprintf("%s %s → %s", + keyStyle.Render(keyStr), + indexKeyStyle.Render(fmt.Sprintf("[%s]", idxKeyStr)), + dimStyle.Render(valueStr)) + } else { + line = fmt.Sprintf("%s → %s", + keyStyle.Render(keyStr), + dimStyle.Render(valueStr)) + } + + if i == m.cursor { + sb.WriteString(selectedStyle.Render("> ")) + sb.WriteString(line) + } else { + sb.WriteString(" ") + sb.WriteString(line) + } + sb.WriteString("\n") + } + + // Scroll indicator (always show position info) + sb.WriteString(dimStyle.Render(fmt.Sprintf("\n[%d-%d of %d]", start+1, end, len(m.entries)))) + } + + sb.WriteString("\n") + sb.WriteString(helpStyle.Render("↑/↓: navigate • enter: view details • esc: back • q: quit")) + + return sb.String() +} + +// renderEntryDetailView renders a single entry in detail. +func renderEntryDetailView(m model) string { + var sb strings.Builder + + if m.selectedEntry >= len(m.entries) { + sb.WriteString(errorStyle.Render("Invalid entry selection")) + return sb.String() + } + + contentLines := m.detailLines + if len(contentLines) == 0 { + // Safety fallback (should normally be populated when entering detail view). + contentLines = buildEntryDetailLines(m.entries[m.selectedEntry]) + } + + // Calculate visible range + visibleLines := m.visibleDetailLines() + totalLines := len(contentLines) + start := min(m.detailScrollOffset, totalLines) + end := min(start+visibleLines, totalLines) + + // Render header + sb.WriteString(titleStyle.Render(fmt.Sprintf("Entry %d of %d", m.selectedEntry+1, len(m.entries)))) + sb.WriteString("\n\n") + + // Render visible content + for i := start; i < end; i++ { + sb.WriteString(contentLines[i]) + sb.WriteString("\n") + } + + // Scroll indicator + if totalLines > visibleLines { + scrollPct := 0 + if totalLines-visibleLines > 0 { + scrollPct = (m.detailScrollOffset * 100) / (totalLines - visibleLines) + } + sb.WriteString(dimStyle.Render(fmt.Sprintf("\n[Line %d-%d of %d (%d%%)]", + start+1, end, totalLines, scrollPct))) + } + + sb.WriteString("\n") + sb.WriteString(helpStyle.Render("↑/↓: scroll • PgUp/PgDn: page • g/G: top/bottom • esc: back")) + + return sb.String() +} diff --git a/dex/testing/loadbot/go.mod b/dex/testing/loadbot/go.mod index c0045d0a03..6f0fdf8b6c 100644 --- a/dex/testing/loadbot/go.mod +++ b/dex/testing/loadbot/go.mod @@ -30,7 +30,7 @@ require ( github.com/athanorlabs/go-dleq v0.1.0 // indirect github.com/bisoncraft/go-monero v0.1.1 // indirect github.com/bisoncraft/op-geth v0.0.0-20250729074358-3cfe4f15e91c // indirect - github.com/bits-and-blooms/bitset v1.20.0 // indirect + github.com/bits-and-blooms/bitset v1.22.0 // indirect github.com/btcsuite/btcd v0.24.2-beta.rc1.0.20240625142744-cc26860b4026 // indirect github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect github.com/btcsuite/btcd/btcutil v1.1.5 // indirect @@ -141,7 +141,7 @@ require ( github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/rivo/uniseg v0.2.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/stretchr/testify v1.10.0 // indirect diff --git a/dex/testing/loadbot/go.sum b/dex/testing/loadbot/go.sum index ed77784668..3c4c899b66 100644 --- a/dex/testing/loadbot/go.sum +++ b/dex/testing/loadbot/go.sum @@ -124,8 +124,8 @@ github.com/bisoncraft/go-monero v0.1.1 h1:XfZYfUigkdTeU8oVq5X08X8tO59EyVwRKDIXbk github.com/bisoncraft/go-monero v0.1.1/go.mod h1:HE9dCbZhK0tVgMfQoYOVX8cP0lihcg6pHojBF+YeOXo= github.com/bisoncraft/op-geth v0.0.0-20250729074358-3cfe4f15e91c h1:cmK4HxTpEutYvsKiRlsibMuzdgAr9vRokgFLTvrCMZ0= github.com/bisoncraft/op-geth v0.0.0-20250729074358-3cfe4f15e91c/go.mod h1:AVdw4MgxUWZ0mhB+VxKOfXH2Z6WJA6rX6YrYrY2bii0= -github.com/bits-and-blooms/bitset v1.20.0 h1:2F+rfL86jE2d/bmw7OhqUg2Sj/1rURkBn3MdfoPyRVU= -github.com/bits-and-blooms/bitset v1.20.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/bits-and-blooms/bitset v1.22.0 h1:Tquv9S8+SGaS3EhyA+up3FXzmkhxPGjQQCkcs2uw7w4= +github.com/bits-and-blooms/bitset v1.22.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/bkielbasa/cyclop v1.2.0/go.mod h1:qOI0yy6A7dYC4Zgsa72Ppm9kONl0RoIlPbzot9mhmeI= github.com/bluele/gcache v0.0.2/go.mod h1:m15KV+ECjptwSPxKhOhQoAFQVtUFjTVkc3H8o0t/fp0= @@ -999,8 +999,9 @@ github.com/quasilyte/regex/syntax v0.0.0-20200407221936-30656e2c4a95/go.mod h1:r github.com/quasilyte/regex/syntax v0.0.0-20200805063351-8f842688393c/go.mod h1:rlzQ04UMyJXu/aOvhd8qT+hvDrFpiwqp8MRXDY9szc0= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= diff --git a/go.mod b/go.mod index 180cd32332..c2438a2409 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,8 @@ require ( github.com/btcsuite/btcwallet/wallet/txauthor v1.3.5 github.com/btcsuite/btcwallet/walletdb v1.4.4 github.com/btcsuite/btcwallet/wtxmgr v1.5.4 + github.com/charmbracelet/bubbletea v1.3.10 + github.com/charmbracelet/lipgloss v1.1.0 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc github.com/dchest/blake2b v1.0.0 github.com/dcrlabs/bchwallet v0.0.0-20240114124852-0e95005810be @@ -86,6 +88,11 @@ require ( decred.org/dcrwallet v1.7.0 // indirect github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 // indirect github.com/asdine/storm/v3 v3.2.1 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect + github.com/charmbracelet/x/ansi v0.10.1 // indirect + github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect + github.com/charmbracelet/x/term v0.2.1 // indirect github.com/decred/dcrd/blockchain/stake/v3 v3.0.0 // indirect github.com/decred/dcrd/database/v2 v2.0.2 // indirect github.com/decred/dcrd/dcrec/secp256k1/v3 v3.0.0 // indirect @@ -95,11 +102,18 @@ require ( github.com/decred/dcrdata/v7 v7.0.0 // indirect github.com/decred/dcrtime v0.0.0-20191018193024-8d8b4ef0458e // indirect github.com/emicklei/dot v1.6.2 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab // indirect github.com/google/trillian v1.4.1 // indirect github.com/gorilla/schema v1.1.0 // indirect github.com/h2non/go-is-svg v0.0.0-20160927212452-35e8c4b0612c // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/marcopeereboom/sbox v1.1.0 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/termenv v0.16.0 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect ) replace github.com/btcsuite/btcd/btcec/v2 v2.3.4 => github.com/martonp/btcd/btcec/v2 v2.0.0-20250528172049-6b252bb1b6a1 @@ -112,7 +126,7 @@ require ( github.com/StackExchange/wmi v1.2.1 // indirect github.com/VictoriaMetrics/fastcache v1.13.0 // indirect github.com/aead/siphash v1.0.1 // indirect - github.com/bits-and-blooms/bitset v1.20.0 // indirect + github.com/bits-and-blooms/bitset v1.22.0 // indirect github.com/btcsuite/btcwallet/wallet/txrules v1.2.2 // indirect github.com/btcsuite/btcwallet/wallet/txsizes v1.2.5 // indirect github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect @@ -177,7 +191,7 @@ require ( github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/rivo/uniseg v0.2.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/stretchr/testify v1.10.0 // indirect diff --git a/go.sum b/go.sum index b233608ecf..1a6463d601 100644 --- a/go.sum +++ b/go.sum @@ -192,6 +192,8 @@ github.com/aws/aws-sdk-go v1.36.30/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2z github.com/aws/aws-sdk-go v1.37.0/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/benbjohnson/clock v1.0.3/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= @@ -204,8 +206,8 @@ github.com/bisoncraft/go-monero v0.1.1 h1:XfZYfUigkdTeU8oVq5X08X8tO59EyVwRKDIXbk github.com/bisoncraft/go-monero v0.1.1/go.mod h1:HE9dCbZhK0tVgMfQoYOVX8cP0lihcg6pHojBF+YeOXo= github.com/bisoncraft/op-geth v0.0.0-20250729074358-3cfe4f15e91c h1:cmK4HxTpEutYvsKiRlsibMuzdgAr9vRokgFLTvrCMZ0= github.com/bisoncraft/op-geth v0.0.0-20250729074358-3cfe4f15e91c/go.mod h1:AVdw4MgxUWZ0mhB+VxKOfXH2Z6WJA6rX6YrYrY2bii0= -github.com/bits-and-blooms/bitset v1.20.0 h1:2F+rfL86jE2d/bmw7OhqUg2Sj/1rURkBn3MdfoPyRVU= -github.com/bits-and-blooms/bitset v1.20.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/bits-and-blooms/bitset v1.22.0 h1:Tquv9S8+SGaS3EhyA+up3FXzmkhxPGjQQCkcs2uw7w4= +github.com/bits-and-blooms/bitset v1.22.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= github.com/bkielbasa/cyclop v1.2.0/go.mod h1:qOI0yy6A7dYC4Zgsa72Ppm9kONl0RoIlPbzot9mhmeI= @@ -280,6 +282,18 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/charithe/durationcheck v0.0.6/go.mod h1:SSbRIBVfMjCi/kEB6K65XEA83D6prSM8ap1UCpNKtgg= github.com/charithe/durationcheck v0.0.7/go.mod h1:SSbRIBVfMjCi/kEB6K65XEA83D6prSM8ap1UCpNKtgg= +github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= +github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= +github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= +github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= +github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ= +github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/chavacava/garif v0.0.0-20210405163807-87a70f3d418b/go.mod h1:Qjyv4H3//PWVzTeCezG2b9IRn6myJxJSr4TD/xo6ojU= github.com/chavacava/garif v0.0.0-20210405164556-e8a0a408d6af/go.mod h1:Qjyv4H3//PWVzTeCezG2b9IRn6myJxJSr4TD/xo6ojU= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= @@ -608,6 +622,8 @@ github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go. github.com/envoyproxy/protoc-gen-validate v0.0.14/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.3.0-java/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/esimonov/ifshort v1.0.2/go.mod h1:yZqNJUrNn20K8Q9n2CrjTKYyVEmX209Hgu+M1LBpeZE= github.com/etcd-io/gofail v0.0.0-20190801230047-ad7f989257ca/go.mod h1:49H/RkXP8pKaZy4h0d+NW16rSLhyVBt4o6VLJbmOqDE= github.com/ethereum/c-kzg-4844/v2 v2.1.5 h1:aVtoLK5xwJ6c5RiqO8g8ptJ5KU+2Hdquf6G3aXiHh5s= @@ -1133,6 +1149,8 @@ github.com/ltcsuite/ltcd/ltcutil v1.1.4-0.20240131072528-64dfa402637a h1:fQ9S26c github.com/ltcsuite/ltcd/ltcutil v1.1.4-0.20240131072528-64dfa402637a/go.mod h1:z8txd/ohBFrOMBUT70K8iZvHJD/Vc3gzx+6BP6cBxQw= github.com/ltcsuite/ltcd/ltcutil/psbt v1.1.1-0.20240131072528-64dfa402637a h1:vGmxYaHruD6GcoT6ui/4CulFqdnwfYoKZ+At84Uy10w= github.com/ltcsuite/ltcd/ltcutil/psbt v1.1.1-0.20240131072528-64dfa402637a/go.mod h1:zcnG/wmZrTcQGTJIBgrG4Fu2ljZcV0DvZ8tzfCvU91o= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= @@ -1161,6 +1179,8 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= @@ -1214,6 +1234,12 @@ github.com/moricho/tparallel v0.2.1/go.mod h1:fXEIZxG2vdfl0ZF8b42f5a78EhjjD5mX8q github.com/mozilla/scribe v0.0.0-20180711195314-fb71baf557c1/go.mod h1:FIczTrinKo8VaLxe6PWTPEXRXDIHz2QAwiaBaP5/4a8= github.com/mozilla/tls-observatory v0.0.0-20180409132520-8791a200eb40/go.mod h1:SrKMQvPiws7F7iqYp8/TX+IhxCYhzr6N/1yb8cwHsGk= github.com/mozilla/tls-observatory v0.0.0-20210209181001-cf43108d6880/go.mod h1:FUqVoUPHSEdDR0MnFM3Dh8AU0pZHLXUD127SAJGER/s= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-proto-validators v0.0.0-20180403085117-0950a7990007/go.mod h1:m2XC9Qq0AlmmVksL6FktJCdTYyLk7V3fKyp0sl1yWQo= @@ -1388,8 +1414,9 @@ github.com/quasilyte/regex/syntax v0.0.0-20200407221936-30656e2c4a95/go.mod h1:r github.com/quasilyte/regex/syntax v0.0.0-20200805063351-8f842688393c/go.mod h1:rlzQ04UMyJXu/aOvhd8qT+hvDrFpiwqp8MRXDY9szc0= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= @@ -1560,6 +1587,8 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1: github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= @@ -1934,6 +1963,7 @@ golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=