Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ forgewatch/
dbus_service.py D-Bus interface (dbus-next)
config.py TOML config loading + validation
url_opener.py Shared URL opener (XDG portal + xdg-open)
cli/ Management subcommands (setup, service, uninstall)
cli/ Management subcommands (setup, service, uninstall, completions)
indicator/ System tray icon + popup window (GTK3, separate process)
```

Expand Down
23 changes: 15 additions & 8 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Key traits:
```
__main__.py CLI entry point -- dispatches to CLI subcommands or daemon
|
+---> cli/ Management subcommands (setup, service, uninstall)
+---> cli/ Management subcommands (setup, service, uninstall, completions)
| __init__.py Argparse parser + run_cli() dispatch
| setup.py Interactive setup wizard (config + systemd)
| service.py Systemd service management (start/stop/status/...)
Expand Down Expand Up @@ -56,7 +56,7 @@ indicator/ Separate process -- system tray icon + popup window

| Module | Role |
|---|---|
| `__main__.py` | CLI entry point. Builds a unified argparse parser with daemon flags (`-c`, `-v`) and management subcommands (`setup`, `service`, `uninstall`). When `args.command` is set, dispatches to `cli.dispatch()`. Otherwise starts the daemon. |
| `__main__.py` | CLI entry point. Builds a unified argparse parser with daemon flags (`-c`, `-v`) and management subcommands (`setup`, `service`, `uninstall`, `completions`). When `args.command` is set, dispatches to `cli.dispatch()`. Otherwise starts the daemon. Also integrates `shtab` for shell-completion metadata on the `--config` flag. |
| `cli/__init__.py` | Registers subcommands via `add_subcommands()`, dispatches via `dispatch()`. Also provides `build_parser()` and `run_cli()` for standalone/test use. |
| `cli/setup.py` | Interactive setup wizard. Config file creation (token, username, poll interval, repos), systemd service installation, and enable+start. Supports `--config-only` and `--service-only` flags. |
| `cli/service.py` | Thin CLI layer over `_systemd.py`. Actions: `install`, `start`, `stop`, `restart`, `status`, `enable`, `disable`. Manages both daemon and indicator services. |
Expand Down Expand Up @@ -118,6 +118,9 @@ uv run forgewatch service install # install systemd unit files
uv run forgewatch service enable # enable autostart
uv run forgewatch service disable # disable autostart
uv run forgewatch uninstall # remove services + optionally config
uv run forgewatch completions bash # generate bash completions
uv run forgewatch completions zsh # generate zsh completions
uv run forgewatch completions tcsh # generate tcsh completions

# Install as systemd user service
systemctl --user enable --now forgewatch
Expand Down Expand Up @@ -199,7 +202,7 @@ uv run mypy forgewatch
| `systemd/forgewatch.service` | Systemd user unit file for the daemon. |
| `systemd/forgewatch-indicator.service` | Systemd user unit file for the indicator. Depends on the daemon service. |
| `forgewatch/indicator/` | System tray indicator package. Separate process, connects to daemon over D-Bus. Requires GTK3/AppIndicator3/gbulb. |
| `forgewatch/cli/` | CLI management subcommands package. Setup wizard, service management, uninstall. Stdlib only (no extra deps). |
| `forgewatch/cli/` | CLI management subcommands package. Setup wizard, service management, uninstall, shell completions. The `completions` subcommand uses `shtab`; all other subcommands are stdlib only. |
| `forgewatch/cli/systemd/` | Bundled `.service` files accessed via `importlib.resources`. |
| `forgewatch/url_opener.py` | Shared URL opener (XDG portal + xdg-open fallback). Used by both notifier and indicator. |
| `docs/` | Architecture, configuration, development, and module documentation. |
Expand Down Expand Up @@ -254,19 +257,23 @@ in `dbus_service.py`), also update:

### Adding/modifying CLI subcommands

The CLI package (`forgewatch.cli`) uses stdlib only (no extra deps). Key patterns:
The CLI package (`forgewatch.cli`) uses stdlib only (no extra deps) for most
subcommands. The `completions` subcommand uses `shtab`. Key patterns:

1. **Shared helpers** are in `_output.py`, `_prompts.py`, `_checks.py`, and
`_systemd.py`. These are pure-Python modules with no async code.
2. **Subcommand handlers** are in `setup.py`, `service.py`, and `uninstall.py`.
Each exports a single `run_*()` entry point.
Each exports a single `run_*()` entry point. The `completions` subcommand
is handled inline in `dispatch()` via `shtab.complete()`.
3. **Parser and dispatch** are in `__init__.py` (`add_subcommands()` + `dispatch()`).
`build_parser()` and `run_cli()` are also provided for standalone/test use.
Subcommand modules are imported lazily inside `dispatch()` to avoid loading
unused code.
4. **Unified parser** in `__main__.py` builds a single argparse parser with both
daemon flags (`-c`, `-v`) and management subcommands. When `args.command` is
not `None`, it dispatches to `cli.dispatch(args)`.
4. **Unified parser** in `__main__.py` (`build_full_parser()`) builds a single
argparse parser with both daemon flags (`-c`, `-v`) and management
subcommands. It also attaches `shtab.FILE` completion metadata to the
`--config` flag. When `args.command` is not `None`, it dispatches to
`cli.dispatch(args)`.
5. **Bundled service files** live in `cli/systemd/` and are read via
`importlib.resources.files("forgewatch.cli.systemd")`.

Expand Down
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,24 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.5.0] - 2026-03-13

### Added

- Shell completion support -- new `forgewatch completions <shell>` subcommand generates Bash, Zsh, or tcsh completions via `shtab`, with file-path completion for `--config`
- Pagination cap warning -- the poller now logs a warning when the page limit is reached and more results are available, suggesting the user narrow their repo filter
- Configurable indicator settings via a new `[indicator]` TOML section -- `reconnect_interval`, `window_width`, and `max_window_height` can now be tuned without code changes
- Unknown config key warnings -- `load_config()` now logs a warning for any unrecognised top-level key, helping catch typos early
- Repo-based notification grouping via a new `[notifications]` config section -- set `grouping = "repo"` to receive per-repository summary notifications instead of a single flat list
- Per-repo notification overrides via `[notifications.repos."owner/repo"]` -- disable notifications for noisy repos (`enabled = false`), override urgency (`urgency = "critical"`), or set a custom individual-vs-summary threshold per repository

### Changed

- Improved first-run experience -- `ConfigError` is now caught and displayed as a user-friendly log message instead of a raw traceback. Missing config suggests running `forgewatch setup`, invalid config suggests checking the config file. The daemon exits cleanly with code 1
- Logging is now initialised before config loading so that config errors are properly formatted
- Config validation now collects all errors and reports them in a single `ConfigError`, so users can fix every problem in one pass instead of playing whack-a-mole
- Validation error messages now include actionable hints (e.g. `ghp_` token prefix example, `GitHub recommends 300s` for poll interval, `octocat/Hello-World` for repo format)

## [1.4.1] - 2026-03-12

### Fixed
Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,9 @@ forgewatch service start|stop|restart
forgewatch service install # install systemd unit files
forgewatch service enable|disable # toggle autostart
forgewatch uninstall # remove services + optionally config
forgewatch completions bash # generate bash completions
forgewatch completions zsh # generate zsh completions
forgewatch completions tcsh # generate tcsh completions
```

---
Expand Down Expand Up @@ -288,7 +291,7 @@ patterns, CI pipeline details, and project structure.

| Module | Description |
|---|---|
| [CLI](docs/modules/cli.md) | Management subcommands (setup, service, uninstall) |
| [CLI](docs/modules/cli.md) | Management subcommands (setup, service, uninstall, completions) |
| [Config](docs/modules/config.md) | Configuration loading and validation |
| [Poller](docs/modules/poller.md) | GitHub API client, pagination, rate limiting |
| [Store](docs/modules/store.md) | In-memory state store with diff computation |
Expand Down
17 changes: 17 additions & 0 deletions config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,20 @@ repos = []
# Use "light" for light desktop panels (dark icons on light background).
# Use "dark" for dark desktop panels (light icons on dark background).
# icon_theme = "light"

# [notifications]
# grouping = "flat" # "flat" (default) or "repo"
#
# [notifications.repos."owner/repo"]
# enabled = true # Set to false to suppress notifications
# urgency = "normal" # "low", "normal", or "critical"
# threshold = 3 # Individual vs summary threshold

# ---------------------------------------------------------------------------
# Indicator settings (system tray process)
# ---------------------------------------------------------------------------

# [indicator]
# reconnect_interval = 10 # Seconds between reconnect attempts (default: 10)
# window_width = 400 # Popup window width in pixels (default: 400)
# max_window_height = 500 # Maximum popup window height in pixels (default: 500)
22 changes: 14 additions & 8 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,10 +161,11 @@ See [modules/indicator.md](modules/indicator.md) for the full API reference.

### CLI Management (`cli/`)

A management interface providing `setup`, `service`, and `uninstall`
subcommands for installing and managing ForgeWatch as a systemd user
service. Uses stdlib only (no extra dependencies beyond the Python standard
library). The package consists of:
A management interface providing `setup`, `service`, `uninstall`, and
`completions` subcommands for installing and managing ForgeWatch as a
systemd user service. Uses stdlib only for most subcommands; the
`completions` subcommand uses `shtab` for shell-completion generation.
The package consists of:

- **Parser and dispatch** (`__init__.py`) -- argparse subcommand parser with
lazy imports to avoid loading unused code.
Expand All @@ -184,10 +185,15 @@ library). The package consists of:
- **Bundled service files** (`systemd/`) -- `.service` files accessed via
`importlib.resources`, allowing installation from PyPI packages without a git
checkout.
- **Shell completions** -- the `completions` subcommand generates Bash, Zsh,
or tcsh completion scripts via `shtab`. The unified parser in `__main__.py`
(`build_full_parser()`) attaches `shtab.FILE` metadata to the `--config`
flag for file-path completion.

Subcommand detection happens in `__main__.py` by checking `sys.argv[1]`
against a known set of command names before the daemon argparse runs, ensuring
full backward compatibility with existing daemon flags (`-c`, `-v`).
Subcommand detection happens in `__main__.py` via the unified argparse
parser. When `args.command` is not `None`, the request is dispatched to
`cli.dispatch(args)`. Otherwise the daemon starts, ensuring full backward
compatibility with existing daemon flags (`-c`, `-v`).

See [modules/cli.md](modules/cli.md) for the full API reference.

Expand Down Expand Up @@ -326,7 +332,7 @@ The system is designed to be resilient to transient failures:
| HTTP 401 | Raise `AuthError` immediately (bad token, no point retrying) |
| HTTP 403 | Respect `Retry-After` header, retry once, then return empty |
| Rate limit exhausted | Preemptively wait until reset time before making request |
| Invalid config | Raise `ConfigError` at startup (fail fast) |
| Invalid config | Catch `ConfigError`, log user-friendly message with remediation hint (`forgewatch setup` for missing config, "check your config file" for validation errors), exit with code 1 |

The daemon should never crash from a transient GitHub API issue. It logs the
error and continues with the next poll cycle.
Expand Down
103 changes: 100 additions & 3 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,37 @@ If no config file is found at the resolved path, a `ConfigError` is raised.
| `notification_urgency` | string | No | `"normal"` | Notification urgency: `"low"`, `"normal"`, or `"critical"` |
| `icon_theme` | string | No | `"light"` | Icon theme for the system tray indicator: `"light"` (dark icons for light panels) or `"dark"` (light icons for dark panels) |

### `[notifications]` section

Settings for notification grouping and per-repo overrides. These affect how
desktop notifications are grouped and allow fine-grained control per repository.

| Field | Type | Required | Default | Description |
|---|---|---|---|---|
| `grouping` | string | No | `"flat"` | Grouping mode: `"flat"` (single list) or `"repo"` (grouped by repository) |

#### `[notifications.repos."owner/repo"]` sub-tables

Per-repo notification overrides. Each key is a repository in `owner/name`
format. Repos without an entry use the global defaults.

| Field | Type | Required | Default | Description |
|---|---|---|---|---|
| `enabled` | boolean | No | `true` | Set to `false` to suppress notifications for this repo |
| `urgency` | string | No | `"normal"` | Notification urgency: `"low"`, `"normal"`, or `"critical"` |
| `threshold` | integer | No | `3` | Individual vs. summary threshold for this repo (minimum: 1) |

### `[indicator]` section

Settings for the system tray indicator process. These are read by the
indicator via `load_indicator_config()` and have no effect on the daemon.

| Field | Type | Required | Default | Description |
|---|---|---|---|---|
| `reconnect_interval` | integer | No | `10` | Seconds between D-Bus reconnect attempts (minimum: 1) |
| `window_width` | integer | No | `400` | Popup window width in pixels (minimum: 200) |
| `max_window_height` | integer | No | `500` | Maximum popup window height in pixels (minimum: 200) |

## GitHub token setup

ForgeWatch needs a GitHub personal access token (PAT) to query the
Expand Down Expand Up @@ -128,8 +159,12 @@ config file without a token and supply it via the environment instead.

## Validation rules

All validation happens at config load time. If any rule fails, a `ConfigError`
is raised with a descriptive message.
All validation happens at config load time. Validation collects **all** errors
and raises a single `ConfigError` listing every problem, so you can fix
everything in one pass. Error messages include actionable hints where possible
(e.g. example token prefix, recommended poll interval).

Unrecognised top-level keys produce a log warning (possible typo detection).

- `github_token` -- must be a non-empty string
- `github_username` -- must be a non-empty string
Expand All @@ -146,6 +181,22 @@ is raised with a descriptive message.
- `notification_urgency` -- must be one of `low`, `normal`, `critical` (case-insensitive)
- `icon_theme` -- must be one of `light`, `dark` (case-insensitive)

**`[notifications]` section:**

- `notifications` -- must be a table (if present)
- `notifications.grouping` -- must be one of `flat`, `repo` (case-insensitive)
- `notifications.repos` -- must be a table (if present)
- Each `notifications.repos."owner/repo"` entry must be a table with:
- `enabled` -- must be a boolean
- `urgency` -- must be one of `low`, `normal`, `critical` (case-insensitive)
- `threshold` -- must be an integer >= 1

**`[indicator]` section:**

- `reconnect_interval` -- must be an integer >= 1
- `window_width` -- must be an integer >= 200
- `max_window_height` -- must be an integer >= 200

## Example config

A minimal configuration with only required fields:
Expand Down Expand Up @@ -174,6 +225,21 @@ max_retries = 5
notification_threshold = 5
notification_urgency = "low"
icon_theme = "light"

[notifications]
grouping = "repo"

[notifications.repos."myorg/frontend"]
urgency = "critical"
threshold = 5

[notifications.repos."myorg/noisy-repo"]
enabled = false

[indicator]
reconnect_interval = 10
window_width = 400
max_window_height = 500
```

Using environment variables instead of a token in the file:
Expand Down Expand Up @@ -237,6 +303,27 @@ repos = []
# Use "light" for light desktop panels (dark icons on light background).
# Use "dark" for dark desktop panels (light icons on dark background).
# icon_theme = "light"

# ---------------------------------------------------------------------------
# Notification grouping and per-repo overrides
# ---------------------------------------------------------------------------

# [notifications]
# grouping = "flat" # "flat" (default) or "repo"
#
# [notifications.repos."owner/repo"]
# enabled = true # Set to false to suppress notifications
# urgency = "normal" # "low", "normal", or "critical"
# threshold = 3 # Individual vs summary threshold

# ---------------------------------------------------------------------------
# Indicator settings (system tray process)
# ---------------------------------------------------------------------------

# [indicator]
# reconnect_interval = 10 # Seconds between reconnect attempts (default: 10)
# window_width = 400 # Popup window width in pixels (default: 400)
# max_window_height = 500 # Maximum popup window height in pixels (default: 500)
```

## Runtime changes via SIGHUP
Expand All @@ -258,7 +345,7 @@ immediately.

```python
from pathlib import Path
from forgewatch.config import load_config
from forgewatch.config import load_config, load_indicator_config

# Load from default path
cfg = load_config()
Expand All @@ -282,4 +369,14 @@ print(cfg.max_retries) # 3
print(cfg.notification_threshold) # 3
print(cfg.notification_urgency) # "normal"
print(cfg.icon_theme) # "light"

# Access notification grouping settings
print(cfg.notifications.grouping) # "flat"
print(cfg.notifications.repos) # {} (or dict of RepoNotificationConfig)

# Load indicator-specific config ([indicator] section)
ind = load_indicator_config()
print(ind.reconnect_interval) # 10
print(ind.window_width) # 400
print(ind.max_window_height) # 500
```
2 changes: 1 addition & 1 deletion docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ forgewatch/ # Main package
├── notifier.py # Desktop notifications
├── url_opener.py # Shared URL opener (XDG portal + xdg-open)
├── daemon.py # Main daemon loop
├── cli/ # CLI management subcommands (stdlib only)
├── cli/ # CLI management subcommands (stdlib + shtab)
│ ├── __init__.py # Subcommand parser + dispatch
│ ├── setup.py # Setup wizard (config + systemd)
│ ├── service.py # Service management (start/stop/status/...)
Expand Down
Loading
Loading