Skip to main content

Cross-platform terminal RSS/Atom feed reader with auto-update

Project description

feedsmanager

A cross-platform terminal (TUI) RSS/Atom feed reader built with Textual. Add feeds by name + URL, refresh them on demand (or on an auto-refresh timer), and browse posts — all stored locally as JSON.

Features

  • Add / Edit / Remove feeds (name + RSS or Atom URL) — admin accounts only, from the Admin Panel (see Accounts)
  • Parses both RSS and Atom via feedparser
  • Manual refresh (single feed or all feeds) plus optional auto-refresh on a configurable interval while the app is open
  • A separate background runner that keeps refreshing feeds after you close the app, until you stop it or shut down your machine (see below)
  • Local user accounts (username + password) with a login/signup screen at startup, and per-user read/unread tracking for every post, keyed by a stable UUID on both the feed and the post (see Accounts)
  • Personal categories (c) to group the feed list into folders — your own, private to your account (see Accounts)
  • Admin export/import of feeds + posts as a single JSON file, for backup or moving subscriptions between machines (see Accounts)
  • Browse a feed's posts and open any post link in your default browser
  • Each feed is stored as its own JSON file named by a UUID, containing the feed's name, URL, and full post list (each post also has its own stable UUID, derived from its guid)
  • Storage location defaults to an OS-standard app-data directory, and can be changed at any time from the Settings screen (existing feed files are moved automatically)

Install

python3 -m venv .venv
source .venv/bin/activate        # Windows: .venv\Scripts\activate
pip install -e .

Requires Python 3.9+.

Testing

pip install -e ".[dev]"
pytest
pytest --cov=feedsmanager --cov-report=term-missing

Unit tests cover the non-UI logic: models, config, storage, feed_parser, users (including password hashing and the admin self-heal), audit, and background's process-control logic (mostly against a mocked subprocess, plus one real python -m feedsmanager.runner end-to-end test). All tests run against a tmp_path-isolated config directory via the isolated_paths fixture in tests/conftest.py — none of them touch your real ~/.../feedsmanager data.

feedsmanager/screens/* (the Textual UI) and runner.py's own subprocess-loop body are not covered by this suite — they were verified manually via Textual's pilot while building each feature, and would need pilot-based integration tests to automate, which is out of scope here. Expect the coverage report to reflect that gap honestly rather than have it hidden.

Run

feedsmanager

(or python -m feedsmanager.app if you didn't install the console script)

Keybindings

Key Action
v / Enter View the selected feed's posts
u Update (refresh) the selected feed
U Update (refresh) all feeds
c Manage categories / assign the selected feed to one
s Open settings (storage path, auto-refresh)
p Open the Admin Panel (admin accounts only)
q Quit

Inside the posts list: o / Enter opens the highlighted post in your default browser (and marks it read), r toggles read/unread on the highlighted post, Escape goes back.

Inside the Admin Panel: a add a feed, e edit the selected feed, d delete the selected feed (with confirmation), x export feeds+posts to a file, i import from a file, Escape back to the dashboard.

Inside the Categories screen: a add a category, e rename the selected one, d delete it (with confirmation), Enter assigns the highlighted category to the feed you opened it from, Escape closes.

Accounts

The app opens on a login screen. If you don't have an account yet, use "Sign up" to create one (username must be unique, password must be at least 4 characters) — you're logged straight in afterwards. There's no password reset yet. Accounts created this way are regular (non-admin) accounts.

Every post's read/unread status is tracked per user, keyed by the feed's UUID and the post's own UUID: new posts start unread for everyone, opening a post's link marks it read for the account you're logged in as, and r in the posts list lets you flip it manually either way. Accounts and their read state are stored together in a single users.json file (see paths below); passwords are never stored in plain text, only a salted PBKDF2 hash.

Categories

Press c on the Dashboard to open the Categories screen. With a feed highlighted, it lets you assign that feed to one of your categories (or back to "Uncategorized") — a feed sits in at most one category at a time, same as a folder. From the same screen you can also add, rename, or delete categories outright (a/e/d), even without a feed selected. Categories are entirely personal: they're stored per-account (right alongside your read/unread state, in the same users.json), so organizing your view doesn't affect what anyone else sees, and every account can freely manage its own. Once you have at least one category, the Dashboard's feed list groups itself under category headers automatically; deleting a category just moves its feeds back to Uncategorized rather than touching the feeds themselves.

Admin accounts

Adding, editing, and deleting feeds is restricted to admin accounts, done from the Admin Panel (p), which also shows a log of who added/edited/ deleted which feed and when. Regular accounts can browse feeds, read posts, and toggle read/unread, but can't manage feeds.

A default admin account (admin / admin) is created automatically the first time the app runs, so there's always at least one admin available. Change or don't rely on these credentials for anything sensitive — there's no way to change a password yet, so this default is a known, accepted weak point for now. Admin accounts can only be created via the CLI, never through the in-app Sign up screen:

feedsmanager admin create <username> <password>

Export / Import

From the Admin Panel, x exports every feed (name + URL) and all of its posts, plus a copy of the current settings for reference, into one JSON file. i imports feeds + posts back from a file in that same format.

A few things worth knowing:

  • Settings are export-only. The file includes a snapshot of the storage path and auto-refresh config purely for reference — importing a file never changes local settings, so it's safe to import a backup taken on a different machine without it silently rewriting your config.
  • Duplicates are skipped, not overwritten. A feed in the import file is matched against existing feeds by URL; if it already exists locally, it's left untouched (and reported as skipped) rather than replacing its posts. New feeds are added with their imported post history intact.
  • Import doesn't hit the network — it trusts the posts already in the file, so it works offline and stays fast even for a large export. Refresh imported feeds afterwards (u/U) if you want to catch up on anything newer than the export.

Background runner

The in-app auto-refresh (Settings screen, s) only runs while the TUI is open. To keep feeds refreshing after you close the app — until you stop it or shut down your machine — start the background runner, either from the CLI or from the Settings screen:

feedsmanager background start    # spawns a detached process, refreshes on
                               # the configured auto-refresh interval
feedsmanager background status   # running/paused, pid, last tick, last error
feedsmanager background pause    # keep the process alive but stop refreshing
feedsmanager background resume
feedsmanager background stop     # terminate it

This is a plain background process, not an OS service — it does not register with launchd/systemd/Task Scheduler and will not start itself on login or after a reboot. If you restart your machine, run feedsmanager background start again to resume it.

Its pid file, status file, and log live under <config_dir>/background/ (see paths below):

File Purpose
runner.pid pid of the running background process
state.json status (running/paused), last tick time, last error
control.json one-shot start/stop/pause/resume command inbox
runner.log stdout/stderr of the background process (no rotation)

Storage

By default, feed data is stored under an OS-appropriate app-data directory:

OS Default path
Windows %LOCALAPPDATA%\feedsmanager\feeds
macOS ~/Library/Application Support/feedsmanager/feeds
Linux $XDG_DATA_HOME/feedsmanager/feeds (or ~/.local/share/feedsmanager/feeds)

App preferences (including the current storage path) live in a small config.json:

OS Config path
Windows %APPDATA%\feedsmanager\config.json
macOS ~/Library/Application Support/feedsmanager/config.json
Linux $XDG_CONFIG_HOME/feedsmanager/config.json (or ~/.config/feedsmanager/config.json)

You can point storage at any directory you like from the in-app Settings screen (s) — existing feed files are moved to the new location.

Accounts (and their per-user read/unread state) live alongside config.json in the same config directory, in a single users.json file. Every add/edit/delete feed operation is also appended to audit.log (JSON Lines, one entry per line) in that same directory, viewable from the Admin Panel.

Each feed is one JSON file, <storage_path>/<uuid>.json:

{
  "id": "b3f1...uuid...",
  "name": "Hacker News",
  "url": "https://hnrss.org/frontpage",
  "created_at": "2026-07-08T07:00:00+00:00",
  "last_updated": "2026-07-08T07:29:28+00:00",
  "last_error": null,
  "posts": [
    {
      "id": "c2a4...uuid, derived from guid...",
      "guid": "https://news.ycombinator.com/item?id=...",
      "title": "...",
      "link": "https://...",
      "published": "2026-07-08T06:00:00+00:00",
      "summary": "..."
    }
  ]
}

Project layout

feedsmanager/
  app.py           # Textual App entry point, in-app auto-refresh timer,
                    # `feedsmanager background ...` CLI dispatch
  background.py    # Controller/CLI for the detached background runner
                    # (start/stop/pause/resume/status)
  runner.py        # Entry point for the detached background process itself
  config.py        # OS-aware default paths + config.json load/save
  users.py         # User accounts (incl. admin role) + per-user read/unread
                    # and category state (users.json), `feedsmanager admin ...`
                    # CLI dispatch
  audit.py         # Append-only feed-management operation log (audit.log)
  export_import.py # Admin export/import of feeds+posts as JSON (settings
                    # included for reference on export only)
  models.py        # Feed / Post dataclasses (each with a stable UUID)
  feed_parser.py   # feedparser-based RSS/Atom fetch + normalization
  storage.py       # Feed CRUD, JSON persistence, storage path migration
  screens/
    login.py       # Login screen (startup) + Sign up modal
    dashboard.py   # Main feed list screen (view/update/settings; no CRUD)
    admin_panel.py # Admin-only: add/edit/delete feeds, export/import, log
    export_import.py # Export destination / import source path modals
    categories.py  # Per-user category manager + feed assignment
    category_form.py # Add/rename category modal
    feed_form.py   # Add/Edit feed modal
    feed_posts.py  # Post list for a feed, read/unread column + toggle
    settings.py    # Storage path / auto-refresh settings
    confirm.py     # Yes/No confirmation modal

Contributing

See CONTRIBUTING.md for dev setup, running tests, and PR guidelines.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

feedsmanager-0.1.0.tar.gz (38.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

feedsmanager-0.1.0-py3-none-any.whl (37.3 kB view details)

Uploaded Python 3

File details

Details for the file feedsmanager-0.1.0.tar.gz.

File metadata

  • Download URL: feedsmanager-0.1.0.tar.gz
  • Upload date:
  • Size: 38.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.12

File hashes

Hashes for feedsmanager-0.1.0.tar.gz
Algorithm Hash digest
SHA256 b05ca0cf62dbc93509980d0c23d4ca4a52ccfeb651bc4e49fe001ed27e008417
MD5 67ac47aa1f11de5dbfc95df8dbaec009
BLAKE2b-256 cac5620267289e964adcca14a194610df2680074f6cf8bee36ece7e5f79000f3

See more details on using hashes here.

File details

Details for the file feedsmanager-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: feedsmanager-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 37.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.12

File hashes

Hashes for feedsmanager-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 7732c2acebe898cd66e715a8580ad7d19911b87107a7826213b32d8918005d6b
MD5 bb4d1165474ca2215e279d8c9e97c117
BLAKE2b-256 3f81ee6c5b294d7a70cc64ce26b7d17c2425b9ce684cab62d969a64bb4e8b58a

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page