Skip to content

feat: add cg watch for real-time WebSocket price streaming#15

Merged
khooihzhz merged 9 commits intomainfrom
feat/watch-websocket-streaming
Mar 12, 2026
Merged

feat: add cg watch for real-time WebSocket price streaming#15
khooihzhz merged 9 commits intomainfrom
feat/watch-websocket-streaming

Conversation

@khooihzhz
Copy link
Collaborator

@khooihzhz khooihzhz commented Mar 12, 2026

Summary

  • Add cg watch command that streams live coin prices via CoinGecko's WebSocket API (analyst plan or above, USD prices)
  • Table mode (default) with screen-clearing redraw, animated status dots, and 500ms green/red price flash on direction change
  • NDJSON mode (-o json) for piping into jq, head, or other tools — exits cleanly on broken pipe
  • WebSocket client (internal/ws) with automatic reconnect (exponential backoff 1s→30s + jitter), liveness timeout (60s), and graceful shutdown with unsubscribe
  • --ids validated against /simple/price before connecting — unknown IDs are skipped with a warning
  • --symbols resolved to coin IDs via /search (picks exact case-insensitive match with highest market cap rank)
  • --dry-run shows preflight API requests and WebSocket payloads without connecting
  • Streamer interface in cmd/client_factory.go for test injection
  • Display improvements: FprintBanner(io.Writer) with per-writer color detection, StdoutColorEnabled(), VisibleWidth rune-based fix
  • Mutex held during Close() WebSocket writes to prevent concurrent write race with reconnect

Test plan

  • go test -race ./... passes
  • make lint clean
  • cg watch --ids bitcoin — live table updates with price flash
  • cg watch --ids bitcoin -o json — streams NDJSON
  • cg watch --ids bitcoin -o json | head -5 — clean exit on broken pipe
  • cg watch --ids bitcoin --dry-run — shows preflight requests + WS payloads
  • cg watch --ids bitcoin with demo key — shows plan restricted error
  • cg watch --symbols btc,eth — resolves symbols via /search and streams
  • cg watch --ids fakecoin — warns and exits with helpful error message
  • cg watch --ids bitcoin,fakecoin — warns about invalid ID, streams valid ones
  • cg watch --symbols zzzzz — warns and exits with helpful error message

Add a new `cg watch` command that streams live coin prices via
CoinGecko's WebSocket API (paid plans only). Supports both an
interactive table mode with price flash coloring and an NDJSON
output mode for piping into other tools.

Key features:
- Real-time price updates (~10s) via ActionCable WebSocket protocol
- Table mode with screen-clearing redraw, animated status dots,
  and 500ms green/red price flash on change
- NDJSON mode (`-o json`) with clean broken-pipe handling
- Symbol resolution via `--symbols` flag
- Automatic reconnect with exponential backoff (1s→30s + jitter)
- Graceful shutdown on SIGINT with unsubscribe
- `--dry-run` support showing WebSocket request info
- Liveness timeout (60s) to detect half-open connections

New files: cmd/watch.go, cmd/watch_test.go, internal/ws/client.go,
internal/ws/client_test.go. Modified: client_factory (Streamer
interface), commands catalog, dryrun, display (FprintBanner,
StdoutColorEnabled, VisibleWidth rune fix), docs.
@khooihzhz khooihzhz requested a review from a team March 12, 2026 01:59
- Suppress errcheck warnings for Close() calls in watch command and WS tests
- Update plan-restricted error to: "this command requires a paid plan
  (analyst or above)" for clearer guidance
Align command help text, README section header, and command reference
with the actual error message wording.
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a cg watch command for real-time WebSocket price streaming via CoinGecko's paid WebSocket API. It includes a full WebSocket client with reconnect logic, both table and NDJSON output modes, dry-run support, and display improvements for per-writer color detection and Unicode-aware width measurement.

Changes:

  • New internal/ws package with WebSocket client implementing ActionCable protocol, automatic reconnect with exponential backoff + jitter, and graceful shutdown with unsubscribe
  • New cmd/watch.go command supporting table mode (with animated status dots and price flash) and NDJSON mode (-o json) with broken pipe handling; plus Streamer interface in client_factory.go for test injection
  • Display improvements: FprintBanner(io.Writer) with per-writer color detection, StdoutColorEnabled(), and VisibleWidth fix using utf8.RuneCountInString for correct Unicode measurement

Reviewed changes

Copilot reviewed 12 out of 13 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
internal/ws/client.go WebSocket client: connect, subscribe, readLoop with reconnect, graceful close
internal/ws/client_test.go Comprehensive tests for WS client: happy path, reconnect, goroutine leak, close
cmd/watch.go cg watch command with table/JSON output modes, signal handling, price flash
cmd/watch_test.go Command-level tests: missing flags, demo rejection, dry-run, JSON output
cmd/client_factory.go Streamer interface and newStreamer factory for test injection
cmd/dryrun.go dryRunWSOutput struct and printDryRunWS for --dry-run mode
cmd/commands.go watch command metadata with Transport: "websocket"
internal/display/color.go StdoutIsTerminal(), StdoutColorEnabled(), VisibleWidth Unicode fix
internal/display/banner.go FprintBanner(io.Writer) with per-writer color detection; PrintBanner delegates
go.mod / go.sum Add gorilla/websocket v1.5.3 dependency
README.md Documentation for cg watch command
CLAUDE.md Project structure and conventions update for WebSocket support

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

… write race

gorilla/websocket doesn't support concurrent writers. Close() now holds
mu while sending unsubscribe/close messages, preventing races with
reconnect's subscribe/setTokens writes.
Use ErrPlanRestricted directly instead of wrapping it with a message
that repeats the same information, which produced a doubled error.
- Validate --ids against /simple/price, skip unknown IDs with warning
- Resolve --symbols via /search (picks highest market_cap_rank match)
  instead of /simple/price which returns symbol keys, not coin IDs
- Dry-run now shows preflight API requests
- Improved error messages with helpful hints
- Added tests for invalid IDs, unresolved symbols, rank disambiguation
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 13 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

…watch table

- subscribe() and setTokens() now hold mu during WriteMessage calls,
  matching the pattern in Close() — prevents concurrent write races
- Watch table 24h% column now uses stdout-based color detection instead
  of stderr-based ColorPercent(), so colors are consistent when stderr
  is redirected
@khooihzhz khooihzhz requested a review from sachiew March 12, 2026 04:23
Copy link

@amree amree left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few items worth addressing:

if id := matchSymbol(res.Coins, sym); id != "" {
coinIDs = append(coinIDs, id)
} else {
warnf("could not resolve symbol %q to a coin, skipping\n", sym)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding syscall.SIGTERM here so cg watch shuts down cleanly when killed by a container runtime or process manager (e.g. docker stop, systemctl stop):

Suggested change
warnf("could not resolve symbol %q to a coin, skipping\n", sym)
signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)


case <-dotTicker.C:
dotFrame = (dotFrame + 1) % len(statusDots)
if len(state) > 0 && display.StdoutIsTerminal() {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

\033[4;1H hardcodes the status line at row 4. If the banner format ever changes, this will silently overwrite the wrong line. Worth tracking the row dynamically or using relative cursor movement instead of an absolute row number.

"github.com/coingecko/coingecko-cli/internal/ws"

"github.com/spf13/cobra"
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other paid-only commands use requirePaid() at the top of the command handler for a consistent, user-friendly error message. Here the check happens deeper in ws.Client.Connect() via cfg.IsPaid(), which gives a different (less helpful) error path for demo-plan users. Adding requirePaid() here would keep the UX consistent.

…coded row

- Handle SIGTERM alongside SIGINT for clean container/process manager shutdown
- Check cfg.IsPaid() early in runWatch for consistent error UX with other paid commands
- Add comment noting row 4 dependency on FprintBanner layout
@khooihzhz khooihzhz force-pushed the feat/watch-websocket-streaming branch from 3ce1352 to 3202b48 Compare March 12, 2026 05:01
- Use CoinGecko brand green (#4BCC00) for price flash instead of softer green
- Create API client once before ID/symbol resolution instead of twice
- Rename colorBrandGreen to colorFlashGreen for clarity
@khooihzhz khooihzhz merged commit e702dd3 into main Mar 12, 2026
5 of 6 checks passed
@khooihzhz khooihzhz deleted the feat/watch-websocket-streaming branch March 12, 2026 05:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants