Skip to content

matzalazar/radar

Repository files navigation

radar

Go version License Platform

radar screenshot

The next side project, open-source contribution, or dataset worth exploring is out there — scattered across browser tabs you never get around to checking.

radar is a keyboard-driven terminal feed aggregator that pulls Reddit, GitHub Issues, Kaggle datasets, and RSS/Atom feeds into one distraction-free TUI. No ads, no algorithmic ranking, no tab switching — just the signal you configured, arriving on its own schedule.


Overview

radar polls each source concurrently on a configurable interval and persists items in a local SQLite database. Read/unread state survives across sessions and startup is instant — no waiting for network fetches on launch.

Sources can be added or removed directly from the TUI. Changes take effect immediately without restarting.


Features

  • Five built-in source types — Reddit (public RSS, no key required), GitHub Issues & PRs (REST API v3), Kaggle datasets (API v1), The Hacker News, and any RSS/Atom feed
  • Concurrent polling — each source runs in its own goroutine with an independent schedule; one slow or failing source never blocks others
  • Persistent read/unread state — items are stored in a local SQLite database; an unread badge in the header updates in real time
  • Bookmarks — press b from the list or detail view to save an item for later; bookmarked items show a prefix and are collected in a dedicated Saved tab
  • Source tab bar — switch between a unified "All" view and per-source tabs with /
  • Incremental title search — press / and type; results filter as you go
  • Detail view — full summary, URL, tags, and source metadata; press o to open in the system browser
  • In-TUI source management — press a to open the Sources screen: browse existing sources, delete them, or add new ones through a guided form, all without touching a config file
  • Everforest dark theme — consistent palette via lipgloss; renders cleanly in any 256-color terminal
  • No CGO — the SQLite driver is pure Go (modernc.org/sqlite); installs with a single go install on any platform

Installation

Requires Go 1.25 or later.

go install github.com/matzalazar/radar/cmd/radar@latest

Or build from source:

git clone https://github.com/matzalazar/radar.git
cd radar
make build        # produces ./radar
# or: go build -o radar ./cmd/radar

Quick start

Run radar with no arguments. If no config file is found, radar starts with an empty source list and a prompt to add your first source.

radar

Press a to open the Sources screen and add your first source through the guided form. The source starts polling immediately — no restart needed.


Configuration

radar searches for its configuration file in the following order, stopping at the first match:

Priority Path
1 $RADAR_CONFIG (environment variable)
2 ./radar.toml (working directory)
3 $XDG_CONFIG_HOME/radar/radar.toml
4 ~/.config/radar/radar.toml

A full annotated example is provided in radar.toml.example. Copy it to one of the paths above and edit as needed.

General settings

[general]
poll_on_startup = true                          # fetch all sources immediately on launch
db_path         = "~/.local/share/radar/radar.db"  # SQLite database path; ~ is expanded

Sources

Each [[sources]] block defines one data source. All source types share three common fields:

Field Required Description
type Source type: reddit, github_issues, kaggle, or rss
name Human-readable label shown in the TUI
poll_interval Fetch frequency as a Go duration string: "15m", "1h", "6h", "24h"
tags List of strings attached to every item from this source

Reddit

Fetches new posts from a subreddit using the public RSS endpoint. No API key required.

[[sources]]
type          = "reddit"
name          = "r/golang"
subreddit     = "golang"
poll_interval = "30m"
tags          = ["go"]
Field Required Description
subreddit Subreddit name without /r/

GitHub Issues & PRs

Fetches open issues (and pull requests, which GitHub exposes through the same endpoint) from any public or private repository using the REST API v3.

[[sources]]
type          = "github_issues"
name          = "Bubble Tea"
owner         = "charmbracelet"
repo          = "bubbletea"
labels        = ["help wanted", "good first issue"]
poll_interval = "1h"
tags          = ["go", "oss"]
# token       = "ghp_yourPersonalAccessTokenHere"
Field Required Description
owner GitHub username or organisation
repo Repository name
labels Filter to issues carrying all listed labels
token Personal access token — raises the rate limit from 60 to 5,000 requests/hour and grants access to private repos

Security note: if you set token, restrict the config file's permissions (chmod 600 ~/.config/radar/radar.toml) and never commit it to version control.

Kaggle datasets

Fetches datasets from the Kaggle API v1, sorted by "hottest". Requires a free Kaggle account.

[[sources]]
type          = "kaggle"
name          = "NLP datasets"
search        = "nlp text classification"
poll_interval = "6h"
tags          = ["ml", "nlp"]
Field Required Description
search Free-text query to filter datasets
username Kaggle username (see credential resolution below)
key Kaggle API key (see credential resolution below)

Credential resolution — radar tries the following in order, stopping at the first complete pair:

  1. username and key fields in the [[sources]] block
  2. KAGGLE_USERNAME and KAGGLE_KEY environment variables
  3. ~/.kaggle/kaggle.json — the standard file created by the Kaggle CLI

To set up ~/.kaggle/kaggle.json: sign in to kaggle.com, go to Settings → API → Create New Token, then:

mkdir -p ~/.kaggle
mv ~/Downloads/kaggle.json ~/.kaggle/kaggle.json
chmod 600 ~/.kaggle/kaggle.json

If credentials cannot be resolved, the Kaggle source is skipped at startup with a warning; all other sources continue normally.

RSS / Atom

Fetches items from any standard RSS 2.0 or Atom 1.0 feed.

[[sources]]
type          = "rss"
name          = "The Go Blog"
url           = "https://go.dev/blog/feed.atom"
poll_interval = "1h"
Field Required Description
url Full URL of the RSS or Atom feed

Keyboard shortcuts

List view

Key Action
j / Move cursor down
k / Move cursor up
Enter Open detail view and mark item as read
b Toggle bookmark (★ indicator; appears in the Saved tab)
/ Switch source tabs (including the Saved tab)
/ Start incremental title search
a Open Sources screen (add / delete sources)
s Open Settings screen (reset database)
r Manually refresh all sources now
q / Ctrl+C Quit

Detail view

Key Action
b Toggle bookmark
o Open item URL in the system browser
Esc / Backspace Return to list
q / Ctrl+C Quit

Sources screen

Key Action
j / k Move cursor
Enter (on "Add new source") Open source type wizard
d Delete highlighted source (prompts for confirmation)
y / n Confirm or cancel deletion
Esc / q Return to list

Source type wizard

Key Action
j / k Move cursor
Enter Select source type and open the field form
Esc / q Return to Sources screen

Source field form

Key Action
Tab / Next field
Shift+Tab / Previous field
Ctrl+S Save — source is written to config and starts polling immediately
Esc Return to type selection

Settings screen

Key Action
j / k Move cursor
Enter Select action
d / Delete Delete highlighted source
y / n Confirm or cancel
Esc / q Return to list

Adding a new source type

radar is designed so that new source types can be added by implementing a single interface and wiring it in four places.

1. Implement core.Source in a new file under internal/sources/:

type MySource struct { /* ... */ }

func (s *MySource) ID() string                  { return "mysource:" + s.query }
func (s *MySource) Name() string                { return s.name }
func (s *MySource) PollInterval() time.Duration { return s.pollInterval }
func (s *MySource) Fetch(ctx context.Context) ([]core.Item, error) { /* ... */ }

2. Register it in the factory (internal/sources/factory.go):

// In BuildSource:
case "mysource":
    return NewMySource(cfg.Name, cfg.URL, cfg.PollInterval.Duration, cfg.Tags, nil), nil

// In SourceIDFor:
case "mysource":
    return "mysource:" + cfg.URL

3. Add config validation (config/config.go, inside validate()):

case "mysource":
    if cfg.URL == "" {
        return fmt.Errorf("config: source[%d] (%s): mysource requires url", i, cfg.Name)
    }

4. Add the TUI form (internal/tui/views/setup.go):

// In AvailableSourceTypes:
{"mysource", "My Source", "Short description"},

// In FormFieldsFor:
case "mysource":
    return []FormField{
        {Label: "Name", Required: true},
        {Label: "URL",  Required: true},
        {Label: "Poll interval", Value: "1h"},
    }

That's it — no other changes required. The scheduler, database layer, and TUI list/detail views handle the new type automatically.


Architecture

cmd/radar/main.go
      │
      ├── config.Load()           — parses radar.toml (BurntSushi/toml)
      │
      ├── core.OpenStore()        — opens SQLite (modernc.org/sqlite, pure Go)
      │                             tables: items, sources_state
      │
      ├── sources.BuildSources()  — one Source per [[sources]] block
      │         │
      │         ├── RedditSource         public RSS, no auth
      │         ├── GitHubIssuesSource   REST API v3, optional token
      │         ├── KaggleSource         API v1, basic auth
      │         └── RSSSource            generic RSS/Atom (mmcdole/gofeed)
      │
      ├── core.NewScheduler()     — one goroutine per source
      │         │                   context-cancellable, WaitGroup-tracked
      │         └── program.Send(FetchResultMsg) ──► Bubble Tea event loop
      │
      └── tui.New()               — Bubble Tea model (Update / View / Init)
                │
                ├── views/list.go      item rows with relative timestamps
                ├── views/detail.go    per-source rendering (Markdown/HTML strip)
                ├── views/setup.go     Sources manager + add-source wizard
                ├── views/settings.go  Settings screen (reset DB)
                └── styles/theme.go    Everforest lipgloss palette

Design decisions:

  • Pure-Go SQLite — no CGO, no system libraries; SetMaxOpenConns(1) respects SQLite's single-writer model; schema applied with CREATE TABLE IF NOT EXISTS on every open so the database self-initializes.
  • Deterministic item IDssha256(sourceID + "\x00" + externalID)[:8] encoded as hex; enables INSERT OR IGNORE deduplication without requiring external IDs to be globally unique.
  • Per-source goroutines — each source gets its own context.CancelFunc; sources added at runtime start a new goroutine immediately, removed sources have their context cancelled; scheduler.Stop() waits on a sync.WaitGroup.
  • Unidirectional message flow — the scheduler is entirely external to the TUI model; it delivers results through program.Send(FetchResultMsg{...}), enqueuing into Bubble Tea's event loop with no shared mutable state between goroutines and the model.
  • Custom TomlDuration — implements encoding.TextUnmarshaler so Go duration strings ("1h30m", "15m") appear as plain TOML string values without any special syntax.

Tech stack

Component Library
Language Go 1.25+
TUI framework Bubble Tea v1.3
Terminal styling lipgloss v1.1
List / spinner components bubbles v1.0
Config format BurntSushi/toml v1.6
Database modernc.org/sqlite v1.48 (pure Go, no CGO)
RSS/Atom parsing mmcdole/gofeed v1.3

Development

make build     # compile to ./radar
make test      # run all tests
make vet       # go vet
make lint      # golangci-lint (requires golangci-lint installed)
make coverage  # open HTML coverage report

Tests cover the config parser, the SQLite store, and the source factory. Run with -race in CI.


License

MIT — see LICENSE.

About

Keyboard-driven terminal feed aggregator — Reddit, GitHub Issues, Kaggle and RSS in one TUI. Built with Go, Bubble Tea, and SQLite.

Topics

Resources

License

Code of conduct

Contributing

Stars

Watchers

Forks

Contributors