From a338c7321f292847d4a1ef4ce7303c4f2b62aea0 Mon Sep 17 00:00:00 2001 From: moneycaringcoder Date: Wed, 8 Apr 2026 20:07:44 +0200 Subject: [PATCH 1/8] fix: align color palette with gitstream Tailwind-based scheme --- internal/config/config.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index fc16191..ccf1f0c 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -97,14 +97,14 @@ func Default() Config { MaxBackoff: Duration(30 * time.Second), Theme: ThemeConfig{ - Green: "#00ff88", - Red: "#ff4444", - Dim: "#555555", - Separator: "#333333", - Cursor: "#1a1a2e", - Footer: "#666666", - FlashGreen: "#1a3a2a", - FlashRed: "#3a1a1a", + Green: "#22c55e", + Red: "#ef4444", + Dim: "#6b7280", + Separator: "#3b3b3b", + Cursor: "#1e293b", + Footer: "#6b7280", + FlashGreen: "#1a2e1a", + FlashRed: "#2e1a1a", Star: "#ffaa00", }, } From ecbcdbe1965e66ab004df0b3e6f1940de51715b4 Mon Sep 17 00:00:00 2001 From: moneycaringcoder Date: Wed, 8 Apr 2026 20:09:47 +0200 Subject: [PATCH 2/8] refactor: remove news integration to focus on core crypto data --- internal/news/news.go | 68 -------------------------------------- internal/ui/crypto.go | 66 ++----------------------------------- internal/ui/defiview.go | 9 +----- internal/ui/panel.go | 10 +----- internal/ui/view.go | 72 +---------------------------------------- 5 files changed, 6 insertions(+), 219 deletions(-) delete mode 100644 internal/news/news.go diff --git a/internal/news/news.go b/internal/news/news.go deleted file mode 100644 index 5d783a8..0000000 --- a/internal/news/news.go +++ /dev/null @@ -1,68 +0,0 @@ -package news - -import ( - "encoding/json" - "fmt" - "net/http" - "time" -) - -const url = "https://api.coingecko.com/api/v3/news?page=1" - -// Article holds a single news headline. -type Article struct { - Title string - Description string - Author string - Source string - URL string - Time time.Time -} - -type rawResponse struct { - Data []rawArticle `json:"data"` -} - -type rawArticle struct { - Title string `json:"title"` - Description string `json:"description"` - Author string `json:"author"` - NewsSite string `json:"news_site"` - URL string `json:"url"` - CreatedAt int64 `json:"created_at"` -} - -// Fetch retrieves the latest crypto news headlines. -func Fetch(limit int) ([]Article, error) { - resp, err := http.Get(url) //nolint:gosec - if err != nil { - return nil, fmt.Errorf("news fetch: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("news fetch: status %s", resp.Status) - } - - var raw rawResponse - if err := json.NewDecoder(resp.Body).Decode(&raw); err != nil { - return nil, fmt.Errorf("news decode: %w", err) - } - - if len(raw.Data) > limit { - raw.Data = raw.Data[:limit] - } - - articles := make([]Article, len(raw.Data)) - for i, r := range raw.Data { - articles[i] = Article{ - Title: r.Title, - Description: r.Description, - Author: r.Author, - Source: r.NewsSite, - URL: r.URL, - Time: time.Unix(r.CreatedAt, 0), - } - } - return articles, nil -} diff --git a/internal/ui/crypto.go b/internal/ui/crypto.go index 4294159..4d94d78 100644 --- a/internal/ui/crypto.go +++ b/internal/ui/crypto.go @@ -15,7 +15,6 @@ import ( "github.com/moneycaringcoder/cryptstream-tui/internal/feargreed" "github.com/moneycaringcoder/cryptstream-tui/internal/funding" "github.com/moneycaringcoder/cryptstream-tui/internal/liquidation" - "github.com/moneycaringcoder/cryptstream-tui/internal/news" "github.com/moneycaringcoder/cryptstream-tui/internal/ticker" "github.com/moneycaringcoder/cryptstream-tui/internal/watchlist" ) @@ -40,9 +39,6 @@ type fngMsg feargreed.Index // defiMsg carries DeFi yield pool data. type defiMsg []defiyields.Pool -// newsMsg carries news articles. -type newsMsg []news.Article - // SortCol identifies which column is used for sorting. type SortCol int @@ -108,9 +104,6 @@ type CryptoView struct { marketStats MarketStats defiPools []defiyields.Pool showDefi bool - newsArticles []news.Article - newsOn bool - newsFlash int focused bool DetailOverlay *tuikit.DetailOverlay[ticker.Ticker] @@ -122,7 +115,6 @@ type CryptoView struct { fundingPoller *tuikit.Poller fngPoller *tuikit.Poller defiPoller *tuikit.Poller - newsPoller *tuikit.Poller } // NewCryptoView creates a CryptoView pre-populated with initial ticker data. @@ -138,9 +130,8 @@ func NewCryptoView(initial []ticker.Ticker, cfg *config.Config) *CryptoView { sortAsc: cfg.SortAscending, filterMode: parseFilterMode(cfg.DefaultFilter), panelOn: parsePanelOn(cfg.PanelLayout), - liqFlash: make(map[string]time.Time), - correlations: make(map[string]float64), - newsOn: true, + liqFlash: make(map[string]time.Time), + correlations: make(map[string]float64), } c.Panel = NewMarketPanel(c.styles) @@ -279,7 +270,6 @@ func NewCryptoView(initial []ticker.Ticker, cfg *config.Config) *CryptoView { c.fundingPoller = tuikit.NewPoller(5*time.Minute, func() tea.Cmd { return fetchFundingCmd() }) c.fngPoller = tuikit.NewPoller(30*time.Minute, func() tea.Cmd { return fetchFngCmd() }) c.defiPoller = tuikit.NewPoller(5*time.Minute, func() tea.Cmd { return fetchDefiCmd() }) - c.newsPoller = tuikit.NewPoller(5*time.Minute, func() tea.Cmd { return fetchNewsCmd() }) return c } @@ -290,7 +280,6 @@ func (c *CryptoView) Init() tea.Cmd { fetchFundingCmd(), fetchFngCmd(), fetchDefiCmd(), - fetchNewsCmd(), ) } @@ -319,17 +308,6 @@ func (c *CryptoView) Update(msg tea.Msg) (tuikit.Component, tea.Cmd) { c.rebuildDefiRows() } return c, nil - case newsMsg: - if msg != nil { - newArticles := []news.Article(msg) - if len(c.newsArticles) == 0 || (len(newArticles) > 0 && newArticles[0].Title != c.newsArticles[0].Title) { - c.newsFlash = 20 - } - c.newsArticles = newArticles - c.visibleRows = c.tableVisibleRows() - c.clampCursor() - } - return c, nil case liqMsg: l := liquidation.Liq(msg) c.recentLiqs = append([]liquidation.Liq{l}, c.recentLiqs...) @@ -351,12 +329,6 @@ func (c *CryptoView) Update(msg tea.Msg) (tuikit.Component, tea.Cmd) { } func (c *CryptoView) handleTick(msg tuikit.TickMsg) (tuikit.Component, tea.Cmd) { - // Flash countdowns - if c.newsFlash > 0 { - c.newsFlash-- - } - - // Check pollers var cmds []tea.Cmd if cmd := c.fundingPoller.Check(msg); cmd != nil { cmds = append(cmds, cmd) @@ -367,9 +339,6 @@ func (c *CryptoView) handleTick(msg tuikit.TickMsg) (tuikit.Component, tea.Cmd) if cmd := c.defiPoller.Check(msg); cmd != nil { cmds = append(cmds, cmd) } - if cmd := c.newsPoller.Check(msg); cmd != nil { - cmds = append(cmds, cmd) - } if len(cmds) > 0 { return c, tea.Batch(cmds...) } @@ -532,11 +501,6 @@ func (c *CryptoView) handleKey(msg tea.KeyMsg) (tuikit.Component, tea.Cmd) { c.defiTable.SetCursor(0) } return c, tuikit.Consumed() - case "n": - c.newsOn = !c.newsOn - c.visibleRows = c.tableVisibleRows() - c.clampCursor() - return c, tuikit.Consumed() case "enter": if c.DetailOverlay != nil && c.cursor >= 0 && c.cursor < len(c.sorted) { c.DetailOverlay.Show(c.sorted[c.cursor]) @@ -575,20 +539,7 @@ func (c *CryptoView) handleMouse(msg tea.MouseMsg) (tuikit.Component, tea.Cmd) { c.cursor = c.table.CursorIndex() } case tea.MouseButtonLeft: - newsH := c.newsHeight() - newsStart := c.height - newsH - if newsH > 0 && msg.Y >= newsStart { - lineIdx := msg.Y - newsStart - 1 - if lineIdx >= 0 && lineIdx < 5 && lineIdx < len(c.newsArticles) { - url := c.newsArticles[lineIdx].URL - if url != "" { - return c, func() tea.Msg { - tuikit.OpenURL(url) - return nil - } - } - } - } else if c.table != nil { + if c.table != nil { c.table.Update(msg) c.cursor = c.table.CursorIndex() } @@ -609,7 +560,6 @@ func (c *CryptoView) KeyBindings() []tuikit.KeyBind { {Key: "shift+tab", Label: "Sort column back", Group: "DATA"}, {Key: "s", Label: "Star/unstar symbol", Group: "DATA"}, {Key: "f", Label: "Cycle filter", Group: "DATA"}, - {Key: "n", Label: "Toggle news", Group: "DATA"}, {Key: "d", Label: "DeFi yields", Group: "DATA"}, {Key: "enter", Label: "Coin detail", Group: "DATA"}, {Key: "/", Label: "Search symbols", Group: "SEARCH"}, @@ -998,16 +948,6 @@ func fetchDefiCmd() tea.Cmd { } } -func fetchNewsCmd() tea.Cmd { - return func() tea.Msg { - articles, err := news.Fetch(20) - if err != nil { - return newsMsg(nil) - } - return newsMsg(articles) - } -} - // ConnCmd returns a Cmd that signals connection state. func ConnCmd(connected bool) tea.Cmd { return func() tea.Msg { diff --git a/internal/ui/defiview.go b/internal/ui/defiview.go index 6460c6e..3e68a30 100644 --- a/internal/ui/defiview.go +++ b/internal/ui/defiview.go @@ -19,18 +19,11 @@ func (c *CryptoView) renderDefiTable(tableW int) string { sb.WriteByte('\n') // Table component handles columns, rows, cursor, scroll - defiH := c.height - c.newsHeight() - 1 // minus title line + defiH := c.height - 1 // minus title line c.defiTable.SetSize(tableW, defiH) c.defiTable.SetFocused(true) sb.WriteString(c.defiTable.View()) - // News band - newsH := c.newsHeight() - if newsH > 0 { - sb.WriteString("\n") - sb.WriteString(c.renderNewsBand(s, tableW)) - } - // Footer sb.WriteString("\n") left := " d close j/k scroll" diff --git a/internal/ui/panel.go b/internal/ui/panel.go index 6a773dd..502e3dc 100644 --- a/internal/ui/panel.go +++ b/internal/ui/panel.go @@ -10,17 +10,9 @@ func (c *CryptoView) tableWidth() int { return c.width } -// newsHeight returns the number of lines the news band occupies. -func (c *CryptoView) newsHeight() int { - if !c.newsOn || len(c.newsArticles) == 0 { - return 0 - } - return 7 // top separator + 5 headline lines + bottom separator -} - // tableVisibleRows returns the number of visible rows. func (c *CryptoView) tableVisibleRows() int { - rows := c.height - 2 - c.newsHeight() // header + header separator; status bar is handled by App + rows := c.height - 2 // header + header separator; status bar is handled by App if rows < 0 { rows = 0 } diff --git a/internal/ui/view.go b/internal/ui/view.go index 50894e6..ece51ac 100644 --- a/internal/ui/view.go +++ b/internal/ui/view.go @@ -2,10 +2,6 @@ package ui import ( "strings" - "time" - - "github.com/charmbracelet/lipgloss" - tuikit "github.com/moneycaringcoder/tuikit-go" ) // View renders the full TUI frame as a string. @@ -23,78 +19,12 @@ func (c *CryptoView) View() string { // renderTable renders the main table content using the tuikit.Table component. func (c *CryptoView) renderTable(tableW int) string { - s := c.styles var sb strings.Builder - // Table component handles header, rows, cursor, scroll - tableH := c.height - c.newsHeight() + tableH := c.height c.table.SetSize(tableW, tableH) c.table.SetFocused(c.focused) sb.WriteString(c.table.View()) - // News band sits between table and footer - newsH := c.newsHeight() - if newsH > 0 { - sb.WriteString("\n") - sb.WriteString(c.renderNewsBand(s, tableW)) - sb.WriteString("\n") - sb.WriteString(s.Sep.Render(strings.Repeat("─", tableW))) - } - return sb.String() } - -// renderNewsBand renders the news ticker band. -func (c *CryptoView) renderNewsBand(s Styles, w int) string { - articles := c.newsArticles - if len(articles) == 0 { - return "" - } - - lines := make([]string, 0, 6) - lines = append(lines, s.Sep.Render(strings.Repeat("─", w))) - - newsLines := 5 - agoStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#666666")) - srcStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#ffaa00")).Bold(true) - dotStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#444444")) - titleStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#cccccc")) - flashStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#ffaa00")).Background(lipgloss.Color("#2a2000")) - - for i := 0; i < newsLines; i++ { - if i >= len(articles) { - lines = append(lines, "") - continue - } - a := articles[i] - - ago := tuikit.RelativeTime(a.Time, time.Now()) - agoPad := padLeft(ago, 7) - src := a.Source - dot := " · " - - usedPlain := 1 + 7 + 1 + len(src) + len(dot) - remaining := w - usedPlain - 1 - if remaining < 0 { - remaining = 0 - } - title := a.Title - titleRunes := []rune(title) - if len(titleRunes) > remaining && remaining > 1 { - title = string(titleRunes[:remaining-1]) + "…" - } else if len(titleRunes) > remaining { - title = string(titleRunes[:remaining]) - } - title = padRight(title, remaining) - - if i == 0 && c.newsFlash > 0 { - plainLine := " " + agoPad + " " + src + dot + title + " " - lines = append(lines, flashStyle.Render(plainLine)) - } else { - lines = append(lines, " "+agoStyle.Render(agoPad)+" "+srcStyle.Render(src)+dotStyle.Render(dot)+titleStyle.Render(title)+" ") - } - } - - return strings.Join(lines, "\n") -} - From a40e9433fae096f7378b72843f0a4df9bdf2f9f4 Mon Sep 17 00:00:00 2001 From: moneycaringcoder Date: Wed, 8 Apr 2026 20:10:32 +0200 Subject: [PATCH 3/8] feat: add multi-line header matching gitstream aesthetic --- internal/ui/panel.go | 2 +- internal/ui/view.go | 73 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 73 insertions(+), 2 deletions(-) diff --git a/internal/ui/panel.go b/internal/ui/panel.go index 502e3dc..8135d9a 100644 --- a/internal/ui/panel.go +++ b/internal/ui/panel.go @@ -12,7 +12,7 @@ func (c *CryptoView) tableWidth() int { // tableVisibleRows returns the number of visible rows. func (c *CryptoView) tableVisibleRows() int { - rows := c.height - 2 // header + header separator; status bar is handled by App + rows := c.height - 4 // 2-line app header + table header + separator; status bar handled by App if rows < 0 { rows = 0 } diff --git a/internal/ui/view.go b/internal/ui/view.go index ece51ac..c716935 100644 --- a/internal/ui/view.go +++ b/internal/ui/view.go @@ -1,7 +1,11 @@ package ui import ( + "fmt" "strings" + + "github.com/charmbracelet/lipgloss" + "github.com/moneycaringcoder/cryptstream-tui/internal/ticker" ) // View renders the full TUI frame as a string. @@ -21,10 +25,77 @@ func (c *CryptoView) View() string { func (c *CryptoView) renderTable(tableW int) string { var sb strings.Builder - tableH := c.height + header := c.renderHeader(tableW) + sb.WriteString(header) + sb.WriteByte('\n') + + tableH := c.height - 2 // reserve 2 lines for header c.table.SetSize(tableW, tableH) c.table.SetFocused(c.focused) sb.WriteString(c.table.View()) return sb.String() } + +// renderHeader renders the 2-line header above the table. +func (c *CryptoView) renderHeader(w int) string { + s := c.styles + + // Line 1: title + connection dot + BTC price right-aligned + titleStyle := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#ffffff")) + title := titleStyle.Render("cryptstream") + + dot := s.DotConnected.Render("●") + if !c.connected { + dot = s.DotReconnecting.Render("●") + } + + btcStr := "" + if btcPrice := c.BtcPrice(); btcPrice > 0 { + btcStr = "BTC " + ticker.FormatPrice(btcPrice) + } + + // right side: dot + btc price + rightPlain := " ● " + btcStr + rightStyled := dot + " " + lipgloss.NewStyle().Foreground(lipgloss.Color("#ffffff")).Render(btcStr) + + leftWidth := lipgloss.Width(title) + rightWidth := len(rightPlain) + gap := w - leftWidth - rightWidth + if gap < 1 { + gap = 1 + } + line1 := title + strings.Repeat(" ", gap) + rightStyled + + // Line 2: dim stats — pair count, filter, sort + filterLabel := "all" + switch c.filterMode { + case FilterGainers: + filterLabel = "gainers" + case FilterLosers: + filterLabel = "losers" + } + + sortLabel := "vol" + switch c.sortCol { + case SortPrice: + sortLabel = "price" + case SortChange: + sortLabel = "change" + case SortSymbol: + sortLabel = "symbol" + case SortCorrelation: + sortLabel = "βbtc" + } + if c.sortAsc { + sortLabel += " ↑" + } else { + sortLabel += " ↓" + } + + statsStr := fmt.Sprintf("%d pairs filter:%s sort:%s", len(c.tickers), filterLabel, sortLabel) + dimStyle := lipgloss.NewStyle().Foreground(s.ColorDim) + line2 := dimStyle.Render(statsStr) + + return line1 + "\n" + line2 +} From 35d1d08282b7b9f70c9f44f9e7a8daa217f02d48 Mon Sep 17 00:00:00 2001 From: moneycaringcoder Date: Wed, 8 Apr 2026 20:11:09 +0200 Subject: [PATCH 4/8] fix: improve panel whitespace and spacing for readability --- internal/ui/marketpanel.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/internal/ui/marketpanel.go b/internal/ui/marketpanel.go index 423a869..8b659b0 100644 --- a/internal/ui/marketpanel.go +++ b/internal/ui/marketpanel.go @@ -163,6 +163,7 @@ func (p *MarketPanel) View() string { // Vol Spikes (collapsible, key: 1) if len(ms.VolSpikes) > 0 { + lines = append(lines, "") lines = append(lines, s.PanelBorder.Render(strings.Repeat("─", w))) arrow := "▾" if p.secVolSpikes.Collapsed { @@ -173,7 +174,7 @@ func (p *MarketPanel) View() string { for _, t := range ms.VolSpikes { sym := padRight(t.DisplaySymbol(), 8) ratio := s.VolSpike.Render(fmt.Sprintf("%.1fx", t.VolumeSpikeRatio)) - lines = append(lines, " "+sym+" "+ratio) + lines = append(lines, " "+sym+" "+ratio) } } } @@ -192,6 +193,7 @@ func (p *MarketPanel) View() string { } if len(pairs) > 0 { sort.Slice(pairs, func(i, j int) bool { return pairs[i].rate > pairs[j].rate }) + lines = append(lines, "") lines = append(lines, s.PanelBorder.Render(strings.Repeat("─", w))) arrow := "▾" if p.secFunding.Collapsed { @@ -204,7 +206,7 @@ func (p *MarketPanel) View() string { fp := pairs[i] sym := padRight(fp.sym, 8) rate := s.Negative.Render(fmt.Sprintf("%+.3f%%", fp.rate)) - lines = append(lines, " "+sym+" "+rate) + lines = append(lines, " "+sym+" "+rate) } for i := len(pairs) - 1; i >= 0 && i >= len(pairs)-show; i-- { fp := pairs[i] @@ -213,13 +215,14 @@ func (p *MarketPanel) View() string { } sym := padRight(fp.sym, 8) rate := s.Positive.Render(fmt.Sprintf("%+.3f%%", fp.rate)) - lines = append(lines, " "+sym+" "+rate) + lines = append(lines, " "+sym+" "+rate) } } } } // Separator + lines = append(lines, "") lines = append(lines, s.PanelBorder.Render(strings.Repeat("─", w))) // Gainers / Losers side by side @@ -255,6 +258,7 @@ func (p *MarketPanel) View() string { // Liquidation feed (collapsible, key: 3) if len(p.recentLiqs) > 0 { + lines = append(lines, "") lines = append(lines, s.PanelBorder.Render(strings.Repeat("─", w))) arrow := "▾" if p.secLiqs.Collapsed { From 4bb294d4223fe1b464ab3fc304ca9fba0bd02fe1 Mon Sep 17 00:00:00 2001 From: moneycaringcoder Date: Wed, 8 Apr 2026 21:13:24 +0200 Subject: [PATCH 5/8] feat: replace DetailOverlay with inline Table detail bar Switch from full-screen overlay to a compact 3-line inline detail bar that updates as the cursor moves. Uses tuikit's new Table DetailFunc with Divider and Badge utilities. Removes dead overlay rendering code from main.go. --- cmd/cryptstream/main.go | 96 ----------------------------------------- internal/ui/crypto.go | 19 +++++--- internal/ui/view.go | 42 ++++++++++++++++++ 3 files changed, 54 insertions(+), 103 deletions(-) diff --git a/cmd/cryptstream/main.go b/cmd/cryptstream/main.go index 6d6728b..3fb1016 100644 --- a/cmd/cryptstream/main.go +++ b/cmd/cryptstream/main.go @@ -8,7 +8,6 @@ import ( "time" tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" tuikit "github.com/moneycaringcoder/tuikit-go" "github.com/moneycaringcoder/cryptstream-tui/internal/binance" "github.com/moneycaringcoder/cryptstream-tui/internal/config" @@ -37,14 +36,6 @@ func main() { commandBar := tuikit.NewCommandBar(buildCommands(cryptoView)) - detailOverlay := tuikit.NewDetailOverlay(tuikit.DetailOverlayOpts[ticker.Ticker]{ - Title: "Coin Detail", - Render: func(t ticker.Ticker, w, h int, theme tuikit.Theme) string { - return renderCoinDetail(t, cryptoView, w) - }, - }) - cryptoView.DetailOverlay = detailOverlay - statusLeft := func() string { filterLabel := "" switch cryptoView.FilterMode() { @@ -103,7 +94,6 @@ func main() { tuikit.WithHelp(), tuikit.WithOverlay("Settings", "c", configEditor), tuikit.WithOverlay("Command", ":", commandBar), - tuikit.WithOverlay("Detail", "", detailOverlay), tuikit.WithStatusBar(statusLeft, statusRight), tuikit.WithTickInterval(100*time.Millisecond), tuikit.WithMouseSupport(), @@ -344,92 +334,6 @@ func buildConfigFields(cfg *config.Config, cv *ui.CryptoView) []tuikit.ConfigFie } } -func renderCoinDetail(t ticker.Ticker, cv *ui.CryptoView, w int) string { - labelStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#888888")) - valStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#ffffff")) - posStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#00ff88")) - negStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#ff4444")) - - chgStyle := posStyle - if t.PriceChangePercent < 0 { - chgStyle = negStyle - } - - lines := []string{ - labelStyle.Render("Symbol: ") + valStyle.Render(t.DisplaySymbol()), - labelStyle.Render("Price: ") + valStyle.Render(ticker.FormatPrice(t.LastPrice)), - labelStyle.Render("Change: ") + chgStyle.Render(fmt.Sprintf("%+.2f%%", t.PriceChangePercent)), - labelStyle.Render("High 24h: ") + valStyle.Render(ticker.FormatPrice(t.HighPrice)), - labelStyle.Render("Low 24h: ") + valStyle.Render(ticker.FormatPrice(t.LowPrice)), - labelStyle.Render("Volume: ") + valStyle.Render(ticker.FormatVolume(t.QuoteVolume)), - } - - // Funding rate if available - fr := cv.FundingRate(t.Symbol) - if fr.Rate != 0 { - frStyle := posStyle - if fr.Rate > 0 { - frStyle = negStyle - } - lines = append(lines, labelStyle.Render("Funding: ")+frStyle.Render(fmt.Sprintf("%+.4f%%", fr.Rate))) - } - - // Volume spike - if t.VolumeSpiking { - lines = append(lines, labelStyle.Render("Vol Spike: ")+posStyle.Render(fmt.Sprintf("%.1fx avg", t.VolumeSpikeRatio))) - } - - // Sparkline - hist := cv.PriceHistory(t.Symbol) - if len(hist) > 1 { - lines = append(lines, "") - lines = append(lines, labelStyle.Render("Price Trend:")) - lines = append(lines, renderSparkline(hist, w-2)) - } - - return strings.Join(lines, "\n") -} - -func renderSparkline(data []float64, width int) string { - blocks := []rune{'▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'} - min, max := data[0], data[0] - for _, v := range data { - if v < min { - min = v - } - if v > max { - max = v - } - } - - span := max - min - if span == 0 { - span = 1 - } - - // Sample data to fit width - n := len(data) - if n > width { - step := float64(n) / float64(width) - sampled := make([]float64, width) - for i := range sampled { - sampled[i] = data[int(float64(i)*step)] - } - data = sampled - } - - var sb strings.Builder - style := lipgloss.NewStyle().Foreground(lipgloss.Color("#00ccff")) - for _, v := range data { - idx := int((v - min) / span * float64(len(blocks)-1)) - if idx >= len(blocks) { - idx = len(blocks) - 1 - } - sb.WriteRune(blocks[idx]) - } - return style.Render(sb.String()) -} - func buildCommands(cv *ui.CryptoView) []tuikit.Command { return []tuikit.Command{ { diff --git a/internal/ui/crypto.go b/internal/ui/crypto.go index 4d94d78..0153619 100644 --- a/internal/ui/crypto.go +++ b/internal/ui/crypto.go @@ -106,8 +106,7 @@ type CryptoView struct { showDefi bool focused bool - DetailOverlay *tuikit.DetailOverlay[ticker.Ticker] - Panel *MarketPanel + Panel *MarketPanel table *tuikit.Table defiTable *tuikit.Table @@ -217,11 +216,22 @@ func NewCryptoView(initial []ticker.Ticker, cfg *config.Config) *CryptoView { return nil } + detailFunc := func(row tuikit.Row, rowIdx int, width int, theme tuikit.Theme) string { + if len(row) < 2 { + return "" + } + symbol := row[1] + "USDT" + t := c.tickers[symbol] + return c.renderDetailBar(t, width) + } + c.table = tuikit.NewTable(columns, nil, tuikit.TableOpts{ HeaderStyle: c.styles.Header, CellRenderer: cellRenderer, RowStyler: rowStyler, SortFunc: sortFunc, + DetailFunc: detailFunc, + DetailHeight: 3, }) defiColumns := []tuikit.Column{ @@ -501,11 +511,6 @@ func (c *CryptoView) handleKey(msg tea.KeyMsg) (tuikit.Component, tea.Cmd) { c.defiTable.SetCursor(0) } return c, tuikit.Consumed() - case "enter": - if c.DetailOverlay != nil && c.cursor >= 0 && c.cursor < len(c.sorted) { - c.DetailOverlay.Show(c.sorted[c.cursor]) - return c, tuikit.Consumed() - } case "esc": if c.searchQuery != "" { c.searchQuery = "" diff --git a/internal/ui/view.go b/internal/ui/view.go index c716935..5bfe722 100644 --- a/internal/ui/view.go +++ b/internal/ui/view.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/charmbracelet/lipgloss" + tuikit "github.com/moneycaringcoder/tuikit-go" "github.com/moneycaringcoder/cryptstream-tui/internal/ticker" ) @@ -99,3 +100,44 @@ func (c *CryptoView) renderHeader(w int) string { return line1 + "\n" + line2 } + +// renderDetailBar renders a compact 3-line inline detail for the selected coin. +func (c *CryptoView) renderDetailBar(t ticker.Ticker, w int) string { + s := c.styles + theme := tuikit.DefaultTheme() + + divider := tuikit.Divider(w, theme) + + chgStyle := s.Positive + if t.PriceChangePercent < 0 { + chgStyle = s.Negative + } + + line1 := fmt.Sprintf(" %s %s %s %s", + tuikit.Badge(t.DisplaySymbol(), lipgloss.Color("#ffffff"), true), + tuikit.Badge(ticker.FormatPrice(t.LastPrice), lipgloss.Color("#ffffff"), false), + chgStyle.Bold(true).Render(fmt.Sprintf("%+.2f%%", t.PriceChangePercent)), + lipgloss.NewStyle().Foreground(s.ColorDim).Render("vol "+ticker.FormatVolume(t.QuoteVolume)), + ) + + // Line 2: supplementary info + dim := lipgloss.NewStyle().Foreground(s.ColorDim) + var parts []string + parts = append(parts, dim.Render(fmt.Sprintf("H %s L %s", ticker.FormatPrice(t.HighPrice), ticker.FormatPrice(t.LowPrice)))) + if fr := c.fundingRates[t.Symbol]; fr.Rate != 0 { + frStyle := s.Positive + if fr.Rate > 0 { + frStyle = s.Negative + } + parts = append(parts, dim.Render("fund ")+frStyle.Render(fmt.Sprintf("%+.3f%%", fr.Rate))) + } + if t.VolumeSpiking { + parts = append(parts, s.VolSpike.Render(fmt.Sprintf("%.1fx vol", t.VolumeSpikeRatio))) + } + if corr, ok := c.correlations[t.Symbol]; ok { + parts = append(parts, dim.Render(fmt.Sprintf("βBTC %.2f", corr))) + } + line2 := " " + strings.Join(parts, " ") + + return divider + "\n" + line1 + "\n" + line2 +} From 4f688677913a47b8916626567d607b34434ba11d Mon Sep 17 00:00:00 2001 From: moneycaringcoder Date: Wed, 8 Apr 2026 21:28:06 +0200 Subject: [PATCH 6/8] fix: starred tokens now show correct info in detail bar Symbol resolution from table rows now strips the star prefix before looking up ticker data, fixing watchlist tokens showing wrong/empty info in the inline detail bar, cell renderer, and row styler. --- go.mod | 4 +++- go.sum | 6 ++---- internal/ui/crypto.go | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/go.mod b/go.mod index 7fe786d..bf130e9 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,8 @@ require ( github.com/moneycaringcoder/tuikit-go v0.5.4 ) +replace github.com/moneycaringcoder/tuikit-go => ../tuikit-go + require ( github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/charmbracelet/bubbles v1.0.0 // indirect @@ -30,5 +32,5 @@ require ( github.com/rivo/uniseg v0.4.7 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect golang.org/x/sys v0.38.0 // indirect - golang.org/x/text v0.3.8 // indirect + golang.org/x/text v0.34.0 // indirect ) diff --git a/go.sum b/go.sum index 89ce4ad..915ba4b 100644 --- a/go.sum +++ b/go.sum @@ -32,8 +32,6 @@ github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2J github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= -github.com/moneycaringcoder/tuikit-go v0.5.4 h1:JVd7C7k01NnT5btty6prc6oVTx3LWYJXM1HDtLnZSeU= -github.com/moneycaringcoder/tuikit-go v0.5.4/go.mod h1:NNJ8NSFnHrd4A7dqmb0DO1kA6vtk8jmdCTKYJFL+h50= 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= @@ -50,5 +48,5 @@ golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= diff --git a/internal/ui/crypto.go b/internal/ui/crypto.go index 0153619..794cc1e 100644 --- a/internal/ui/crypto.go +++ b/internal/ui/crypto.go @@ -151,7 +151,7 @@ func NewCryptoView(initial []ticker.Ticker, cfg *config.Config) *CryptoView { s := c.styles symbol := "" if len(row) > 1 { - symbol = row[1] + "USDT" + symbol = strings.TrimPrefix(row[1], "★ ") + "USDT" } cell := row[colIdx] @@ -199,7 +199,7 @@ func NewCryptoView(initial []ticker.Ticker, cfg *config.Config) *CryptoView { if len(row) < 2 { return nil } - symbol := row[1] + "USDT" + symbol := strings.TrimPrefix(row[1], "★ ") + "USDT" t := c.tickers[symbol] if time.Now().Before(t.FlashUntil) && t.Flash != ticker.FlashNeutral { st := flashStyle(c.styles, t.Flash) @@ -220,7 +220,7 @@ func NewCryptoView(initial []ticker.Ticker, cfg *config.Config) *CryptoView { if len(row) < 2 { return "" } - symbol := row[1] + "USDT" + symbol := strings.TrimPrefix(row[1], "★ ") + "USDT" t := c.tickers[symbol] return c.renderDetailBar(t, width) } From 4c0703104953ebd3293227c1b63f1d6fc34ff12f Mon Sep 17 00:00:00 2001 From: moneycaringcoder Date: Wed, 8 Apr 2026 21:44:43 +0200 Subject: [PATCH 7/8] test: add tuitest integration tests for CryptoView 13 screen-based tests covering table rendering, cursor navigation, sort cycling, filter modes, search, star toggle, resize, empty state, ticker updates, and selected ticker using tuikit's tuitest package. --- go.mod | 3 +- go.sum | 2 + internal/ui/crypto_tuitest_test.go | 278 +++++++++++++++++++++++++++++ 3 files changed, 282 insertions(+), 1 deletion(-) create mode 100644 internal/ui/crypto_tuitest_test.go diff --git a/go.mod b/go.mod index bf130e9..e9e5566 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/charmbracelet/bubbletea v1.3.10 github.com/charmbracelet/lipgloss v1.1.0 github.com/gorilla/websocket v1.5.3 - github.com/moneycaringcoder/tuikit-go v0.5.4 + github.com/moneycaringcoder/tuikit-go v0.6.0 ) replace github.com/moneycaringcoder/tuikit-go => ../tuikit-go @@ -29,6 +29,7 @@ require ( 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/rcarmo/go-te v0.1.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect golang.org/x/sys v0.38.0 // indirect diff --git a/go.sum b/go.sum index 915ba4b..8b3a8f1 100644 --- a/go.sum +++ b/go.sum @@ -38,6 +38,8 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU 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/rcarmo/go-te v0.1.0 h1:BH9Ub+A0AVBY5Q00El4QMVaWAMbycVHgMHQI2Kz8J/o= +github.com/rcarmo/go-te v0.1.0/go.mod h1:cLsrtroxCubS+OHHwH0riB6xeNESfntaHEeI1jPAedk= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= diff --git a/internal/ui/crypto_tuitest_test.go b/internal/ui/crypto_tuitest_test.go new file mode 100644 index 0000000..84609ae --- /dev/null +++ b/internal/ui/crypto_tuitest_test.go @@ -0,0 +1,278 @@ +package ui + +import ( + "testing" + + tea "github.com/charmbracelet/bubbletea" + tuikit "github.com/moneycaringcoder/tuikit-go" + "github.com/moneycaringcoder/tuikit-go/tuitest" + "github.com/moneycaringcoder/cryptstream-tui/internal/config" + "github.com/moneycaringcoder/cryptstream-tui/internal/ticker" +) + +// testTickers returns synthetic ticker data for testing. +func testTickers() []ticker.Ticker { + return []ticker.Ticker{ + {Symbol: "BTCUSDT", LastPrice: 67432.10, PriceChangePercent: 2.41, QuoteVolume: 4_200_000_000}, + {Symbol: "ETHUSDT", LastPrice: 3512.50, PriceChangePercent: -1.20, QuoteVolume: 2_100_000_000}, + {Symbol: "SOLUSDT", LastPrice: 142.30, PriceChangePercent: 5.67, QuoteVolume: 800_000_000}, + {Symbol: "DOGEUSDT", LastPrice: 0.1234, PriceChangePercent: -3.45, QuoteVolume: 500_000_000}, + {Symbol: "ADAUSDT", LastPrice: 0.45, PriceChangePercent: 0.12, QuoteVolume: 300_000_000}, + } +} + +// testCryptoApp builds a tuikit.App wrapping a CryptoView with test data. +func testCryptoApp(t testing.TB) (*tuitest.TestModel, *CryptoView) { + t.Helper() + cfg := config.Default() + initial := testTickers() + cv := NewCryptoView(initial, &cfg) + + app := tuikit.NewApp( + tuikit.WithLayout(&tuikit.DualPane{ + Main: cv, + Side: cv.Panel, + SideWidth: 30, + MinMainWidth: 70, + SideRight: true, + ToggleKey: "p", + }), + tuikit.WithStatusBar( + func() string { return " test left" }, + func() string { return "test right " }, + ), + ) + + tm := tuitest.NewTestModel(t, app.Model(), 120, 40) + + // Mark connected so header renders correctly + tm.SendMsg(connMsg{connected: true}) + + return tm, cv +} + +func TestCryptoRendersTable(t *testing.T) { + tm, _ := testCryptoApp(t) + s := tm.Screen() + + tuitest.AssertContains(t, s, "cryptstream") + tuitest.AssertContains(t, s, "BTC") + tuitest.AssertContains(t, s, "ETH") + tuitest.AssertContains(t, s, "SOL") +} + +func TestCryptoRendersHeader(t *testing.T) { + tm, _ := testCryptoApp(t) + s := tm.Screen() + + tuitest.AssertContains(t, s, "cryptstream") + tuitest.AssertContains(t, s, "5 pairs") +} + +func TestCryptoCursorNavigation(t *testing.T) { + tm, cv := testCryptoApp(t) + + // Initial cursor at 0 + if cv.CursorPos() != 0 { + t.Errorf("expected cursor at 0, got %d", cv.CursorPos()) + } + + tm.SendKey("down") + if cv.CursorPos() != 1 { + t.Errorf("expected cursor at 1 after down, got %d", cv.CursorPos()) + } + + tm.SendKey("down") + tm.SendKey("down") + if cv.CursorPos() != 3 { + t.Errorf("expected cursor at 3, got %d", cv.CursorPos()) + } + + tm.SendKey("up") + if cv.CursorPos() != 2 { + t.Errorf("expected cursor at 2 after up, got %d", cv.CursorPos()) + } +} + +func TestCryptoJumpToTopBottom(t *testing.T) { + tm, cv := testCryptoApp(t) + + // Jump to bottom + tm.SendKey("G") + if cv.CursorPos() != cv.VisibleCount()-1 { + t.Errorf("expected cursor at bottom (%d), got %d", cv.VisibleCount()-1, cv.CursorPos()) + } + + // Jump to top + tm.SendKey("g") + if cv.CursorPos() != 0 { + t.Errorf("expected cursor at 0, got %d", cv.CursorPos()) + } +} + +func TestCryptoSortCycle(t *testing.T) { + tm, _ := testCryptoApp(t) + + // Tab cycles sort column + tm.SendKey("tab") + s := tm.Screen() + tuitest.AssertNotEmpty(t, s) + + tm.SendKey("tab") + s = tm.Screen() + tuitest.AssertNotEmpty(t, s) +} + +func TestCryptoFilterCycle(t *testing.T) { + tm, cv := testCryptoApp(t) + + // f cycles filter: all -> gainers -> losers -> all + tm.SendKey("f") + if cv.FilterMode() != FilterGainers { + t.Errorf("expected FilterGainers, got %d", cv.FilterMode()) + } + + tm.SendKey("f") + if cv.FilterMode() != FilterLosers { + t.Errorf("expected FilterLosers, got %d", cv.FilterMode()) + } + + tm.SendKey("f") + if cv.FilterMode() != FilterAll { + t.Errorf("expected FilterAll, got %d", cv.FilterMode()) + } +} + +func TestCryptoSearch(t *testing.T) { + tm, cv := testCryptoApp(t) + + // Enter search mode + tm.SendKey("/") + if !cv.IsSearching() { + t.Error("should be in search mode after /") + } + + // Type search query + tm.Type("btc") + if cv.SearchQuery() != "btc" { + t.Errorf("expected search query 'btc', got '%s'", cv.SearchQuery()) + } + + // Confirm search + tm.SendKey("enter") + if cv.IsSearching() { + t.Error("should exit search mode after enter") + } + + s := tm.Screen() + tuitest.AssertContains(t, s, "BTC") +} + +func TestCryptoSearchCancel(t *testing.T) { + tm, cv := testCryptoApp(t) + + tm.SendKey("/") + tm.Type("xyz") + tm.SendKey("esc") + + if cv.IsSearching() { + t.Error("should exit search mode after esc") + } + if cv.SearchQuery() != "" { + t.Error("search query should be cleared after esc") + } +} + +func TestCryptoStarToggle(t *testing.T) { + _, cv := testCryptoApp(t) + + sym := cv.sorted[0].Symbol + + // Ensure the symbol starts unstarred (real watchlist.json may have it starred) + if cv.Watchlist.IsStarred(sym) { + cv.Watchlist.Toggle(sym) + } + + // Star via key handler + cv.handleKey(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("s")}) + + if !cv.Watchlist.IsStarred(sym) { + t.Errorf("expected %s to be starred", sym) + } + + // Unstar + cv.handleKey(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("s")}) + if cv.Watchlist.IsStarred(sym) { + t.Errorf("expected %s to be unstarred", sym) + } +} + +func TestCryptoResize(t *testing.T) { + tm, _ := testCryptoApp(t) + + // Small size + tm.SendResize(80, 20) + s := tm.Screen() + tuitest.AssertNotEmpty(t, s) + tuitest.AssertContains(t, s, "cryptstream") + + // Very small + tm.SendResize(50, 15) + s = tm.Screen() + tuitest.AssertNotEmpty(t, s) + + // Large + tm.SendResize(200, 50) + s = tm.Screen() + tuitest.AssertContains(t, s, "BTC") +} + +func TestCryptoEmptyState(t *testing.T) { + cfg := config.Default() + cv := NewCryptoView(nil, &cfg) + + app := tuikit.NewApp( + tuikit.WithLayout(&tuikit.DualPane{ + Main: cv, + Side: cv.Panel, + SideWidth: 30, + MinMainWidth: 70, + SideRight: true, + }), + ) + + tm := tuitest.NewTestModel(t, app.Model(), 120, 40) + s := tm.Screen() + tuitest.AssertNotEmpty(t, s) +} + +func TestCryptoTickerUpdate(t *testing.T) { + tm, cv := testCryptoApp(t) + + // Send a price update + updated := ticker.Ticker{ + Symbol: "BTCUSDT", + LastPrice: 70000.00, + PriceChangePercent: 3.81, + QuoteVolume: 5_000_000_000, + } + tm.SendMsg(tickerMsg(updated)) + + // The ticker map should be updated + btc := cv.tickers["BTCUSDT"] + if btc.LastPrice != 70000.00 { + t.Errorf("expected BTC price 70000, got %f", btc.LastPrice) + } +} + +func TestCryptoSelectedTicker(t *testing.T) { + _, cv := testCryptoApp(t) + + tk, ok := cv.SelectedTicker() + if !ok { + t.Fatal("expected a selected ticker") + } + if tk.Symbol == "" { + t.Error("selected ticker should have a symbol") + } +} From adf64da40e1ac612b04fcf5baca8394f459701ee Mon Sep 17 00:00:00 2001 From: moneycaringcoder Date: Wed, 8 Apr 2026 22:08:20 +0200 Subject: [PATCH 8/8] feat: bump tuikit-go to v0.7.0 Picks up Table RowStyler background fix (flash/cursor covers cell text), App.Model() for tuitest, and utility helpers. Removes local replace directive. --- go.mod | 4 +--- go.sum | 2 ++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index e9e5566..03d9db8 100644 --- a/go.mod +++ b/go.mod @@ -6,11 +6,9 @@ require ( github.com/charmbracelet/bubbletea v1.3.10 github.com/charmbracelet/lipgloss v1.1.0 github.com/gorilla/websocket v1.5.3 - github.com/moneycaringcoder/tuikit-go v0.6.0 + github.com/moneycaringcoder/tuikit-go v0.7.0 ) -replace github.com/moneycaringcoder/tuikit-go => ../tuikit-go - require ( github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/charmbracelet/bubbles v1.0.0 // indirect diff --git a/go.sum b/go.sum index 8b3a8f1..8e1ad6b 100644 --- a/go.sum +++ b/go.sum @@ -32,6 +32,8 @@ github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2J github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/moneycaringcoder/tuikit-go v0.7.0 h1:DEI3CGNhndFfpxYHjwDvd/TkDndQfTbFrixMy/QVj3E= +github.com/moneycaringcoder/tuikit-go v0.7.0/go.mod h1:2P2MPQGh/A+vpCcrgh5Taz+XqMlrpPgAsc2cd4W8ucg= 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=