commit 247ce4c62eb7a419967d18ea92a179ed33469859 Author: Ivan Juanes Date: Sat Jul 11 23:26:54 2026 +0100 Initial commit: Peak Monitor system tray app with Tau development framework Peak Monitor is a Linux system tray app that monitors two daily peak periods (09:00-12:00 and 14:00-18:00, Atlantic/Canary time) and sends desktop notifications when each period starts and ends. Files: - peak_monitor.py: Main application (GTK + AyatanaAppIndicator3) - peak-monitor.desktop: Desktop entry for autostart - ROADMAP.md: Development roadmap using Tau as the development framework - .env.example: Environment variable template (API keys) Tech stack: - Python 3 with GTK - Pillow for tray icons - libnotify for desktop notifications - Tau coding agent for development assistance diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..8bec7bb --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +# API Keys (replace with your own) +MISTRAL_API_KEY=your_key_here +MISTRAL_API_ENDPOINT=https://api.mistral.ai/v1/embeddings diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9450866 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# Secrets +.env + +# Python +__pycache__/ +*.pyc + +# Nested repos (managed independently) +tau/ +tau-learning/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..e28697d --- /dev/null +++ b/README.md @@ -0,0 +1,228 @@ +

+ Tau — a Python coding-agent harness inspired by Pi +

+ +

+ A small, readable terminal coding agent — and a working example of how coding agents are built. +

+ +

+ Documentation + · + Quickstart + · + Architecture + · + PyPI + · + Roadmap +

+ +--- + +## What is Tau? + +**Tau is a coding agent that lives in your terminal.** You type requests like +"explain this repo", "add tests", or "fix this stack trace"; Tau can read files, +edit code, run commands, and keep a durable session history while streaming what +it is doing. + +Tau is also meant to be read. It is a teaching project for understanding the +shape of a coding-agent system without starting from a giant production +codebase. + +```text +tau_coding → tau_agent → tau_ai +``` + +- `tau_ai` translates model providers into Tau's provider-neutral stream. +- `tau_agent` owns the portable brain: messages, tools, events, loop, harness, + and session primitives. +- `tau_coding` wraps the brain as a real coding app: CLI, TUI, file/shell tools, + provider config, project instructions, skills, and on-disk sessions. + +The important boundary is: + +```text +AgentHarness = reusable brain +CodingSession = coding-agent environment +TUI = one possible frontend +``` + +The core does not know about Textual, Rich, local config paths, slash commands, +or rendering. Frontends consume events. + +## Install + +Tau is published on PyPI as `tau-ai` and installs a `tau` command. +It requires Python 3.12 or newer. + +```bash +uv tool install tau-ai +tau --version +``` + +Don't have `uv`? Install with `pipx` or `pip` instead: + +```bash +pipx install tau-ai +# or +python -m pip install tau-ai +``` + +If you prefer `uv`, install it with: + +```bash +curl -LsSf https://astral.sh/uv/install.sh | sh +``` + +For local development: + +```bash +git clone https://github.com/alejandro-ao/tau.git +cd tau +uv sync --dev +uv run tau --version +``` + +## Quickstart + +Run Tau from the project you want it to work on: + +```bash +cd my-project +tau +``` + +Then type a request and press **Enter**: + +```text +explain what this project does +``` + +One-shot print mode is useful for scripts and quick prompts: + +```bash +tau -p "summarize the architecture" +tau --cwd /path/to/project -p "find the CLI entry point" +``` + +Tau needs a model provider. Start Tau and connect one with `/login`: + +```bash +tau +``` + +```text +/login +/login openai +/login openai-codex +/model +``` + +Tau ships with support for OpenAI, Anthropic, OpenAI Codex subscription auth, +OpenRouter, Hugging Face, and custom OpenAI-compatible endpoints, including local +models. See the [providers guide](https://twotimespi.dev/guides/providers-and-models/). + +The built-in catalog lives in `src/tau_coding/data/catalog.toml`; add your own +providers and models by dropping a `~/.tau/catalog.toml` with the same schema — +no code changes required. + +## What Tau can do + +- Interactive Textual TUI and non-interactive print mode. +- Built-in coding tools: `read`, `write`, `edit`, and `bash`. +- Durable JSONL sessions under `~/.tau/sessions/` with resume and branching. +- Slash commands for login, model selection, sessions, compaction, export, theme, + and more. +- Project instructions from `AGENTS.md`, `.tau/`, and `.agents/` resources. +- User skills and prompt templates. +- Context accounting, manual compaction, and optional automatic compaction. +- Provider-neutral event rendering for Rich, plain text, JSON, transcripts, and + custom frontends. + +## Philosophy + +Tau follows a few rules: + +- **Small layers beat magic.** Each package has one job and can be read alone. +- **Events are the contract.** Providers, renderers, the TUI, and custom + frontends meet at a typed event stream. +- **The core stays portable.** The reusable harness does not depend on the CLI, + Textual, Rich, or Tau's file layout. +- **Tools are ordinary typed functions.** A tool is a schema plus an async + executor returning a structured result. +- **Sessions are durable and inspectable.** History is append-only JSONL; active + context can be compacted without rewriting the record. +- **Documentation follows implementation.** The public docs explain the result; + `dev-notes/` preserves the phase-by-phase build journal. + +## Use Tau as a library + +```python +from tau_agent import AgentHarness, AgentHarnessConfig + +harness = AgentHarness( + AgentHarnessConfig( + provider=provider, + model="my-model", + system="You are a helpful coding agent.", + tools=tools, + ) +) + +async for event in harness.prompt("Explain this package"): + print(event) +``` + +Because the harness emits events instead of rendering UI directly, the same core +can drive the built-in TUI, print mode, or a frontend you build yourself. + +## Development + +See [CONTRIBUTING.md](CONTRIBUTING.md) for project philosophy, layer boundaries, testing expectations, and pull request guidelines. + +```bash +uv sync --dev +uv run pytest +uv run ruff check . +uv run ruff format --check . +uv run mypy +``` + +Run Tau from the checkout: + +```bash +uv run tau +uv run tau -p "explain this repo" +``` + +Run the Hugo documentation site: + +```bash +cd website +hugo server -D +``` + +Open . Build with `hugo --minify`. + +## Documentation + +User docs are published at and live in +`website/content/`. + +Useful entry points: + +- [What is Tau?](https://twotimespi.dev/what-is-tau/) +- [Quickstart](https://twotimespi.dev/quickstart/) +- [Core concepts](https://twotimespi.dev/concepts/) +- [Architecture overview](https://twotimespi.dev/internals/architecture/) +- [The agent loop & events](https://twotimespi.dev/internals/agent-loop/) +- [CLI reference](https://twotimespi.dev/reference/cli/) + +Tau is under active development. The implementation roadmap is tracked in +[GitHub issue #1](https://github.com/alejandro-ao/tau/issues/1). + +## License + +Tau is released under the [MIT License](LICENSE). diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..e49ac4a --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,214 @@ +# Peak Monitor Development Roadmap + +**Purpose**: This document defines the evolution path for Peak Monitor using Tau as the development framework. Each item must be attempted by invoking Tau with the appropriate prompt, NOT by manual implementation. + +**Journal**: This file serves as both a task list and a development journal. Each milestone accumulates notes as work progresses. + +--- + +## Phase 0: Foundation Skills + +### 0.1 Create Test Code Skill +- [x] Create a generic testing best practices skill for Python projects + - **Prompt**: `"Create a simple, generic test-code skill that provides testing patterns and quality checklists for any Python project"` + - **Success Criteria**: Skill is generic, simple, and improves test quality + - **Location**: `~/.tau/skills/test-code/SKILL.md` + - **Journal**: 2026-07-11 14:00 - Created test-code skill with AAA pattern, pytest patterns, anti-patterns, and quality checklist + - **Decision**: Generic skill preferred over project-specific to maximize reusability across all projects + - **Tau Notes**: Skill follows minimalist philosophy - simple enough to be maintainable, useful enough to improve consistency + +### 0.2 Create Web Search Skill +- [x] Create a web search skill using ddg-search CLI + - **Prompt**: `"Create a web-search skill that uses ddg-search to fetch and present web search results"` + - **Success Criteria**: Skill activates on search patterns and provides useful results + - **Location**: `~/.tau/skills/web-search/SKILL.md` + - **Journal**: 2026-07-11 13:30 - Web search capability added, transforming Tau from local-only to web-connected agent + - **Decision**: Used ddg-search CLI (DuckDuckGo scraper) as it provides structured output and respects privacy + - **Tau Notes**: Installed via bun, globally linked for system-wide availability + - **Lesson**: External CLI tools can be integrated as skills to extend Tau's capabilities significantly + +--- + +## 🎯 Philosophy + +**Tau does the work. We guide Tau.** + +Every task below must be executed by running Tau and giving it instructions. If Tau cannot complete a task, document WHY in the journal entries below. This is how we learn Tau's capabilities. + +--- + +--- + +## Phase 1: Project Structure Refactoring + +### 1.1 Analyze Current Architecture +- [ ] Use Tau to analyze the project and identify structural improvements + - **Prompt**: `"Analyze the project codebase and suggest a better project structure. Do not implement yet."` + - **Success Criteria**: Tau produces a clear analysis document + - **Output**: Architecture analysis in `docs/analysis.md` + +### 1.2 Create Package Structure +- [ ] Use Tau to refactor the monolithic file into a proper Python package + - **Prompt**: `"Refactor the main file into a package structure with separate modules for config, core logic, and GUI. Preserve all functionality."` + - **Expected Structure**: + ``` + package_name/ + ├── __init__.py + ├── __main__.py + ├── config.py + ├── core.py + ├── gui.py + └── notifications.py + ``` + - **Success Criteria**: Code runs identically to original, but is now modular + +### 1.3 Add Configuration File Support +- [ ] Use Tau to add TOML-based configuration + - **Prompt**: `"Add support for a configuration file that allows users to customize settings. Keep existing hardcoded values as defaults."` + - **Success Criteria**: + - New config file is read if present + - Defaults to current behavior if file missing + - Sample config generated + +--- + +## Phase 2: Quality Infrastructure + +### 2.1 Add Type Hints +- [ ] Use Tau to add comprehensive type hints + - **Prompt**: `"Add complete type hints to all functions and modules in the project."` + - **Success Criteria**: `mypy --strict` passes (or Tau explains what can't be typed) + +### 2.2 Create Unit Tests +- [ ] Use Tau to write unit tests for the project + - **Prompt**: `"Create pytest unit tests for the core logic functions. Focus on boundary conditions and edge cases."` + - **Success Criteria**: Tests pass, cover edge cases + +### 2.3 Add Error Handling +- [ ] Use Tau to add robust error handling + - **Prompt**: `"Add error handling for missing dependencies and edge cases with helpful error messages."` + - **Success Criteria**: Clear error messages guide users to resolve issues + +--- + +## Phase 3: Feature Enhancements + +### 3.1 Multiple Profiles +- [ ] Use Tau to add profile support + - **Prompt**: `"Add support for multiple named profiles with different settings. Users should switch via CLI argument or config."` + - **Success Criteria**: Users can define and switch between profiles + +### 3.2 CLI Mode +- [ ] Use Tau to add a CLI interface + - **Prompt**: `"Add a CLI mode that can check current status without GUI."` + - **Success Criteria**: Headless operation works + +### 3.3 Sound Notifications +- [ ] Use Tau to add optional sound alerts + - **Prompt**: `"Add optional sound notification support. Users should be able to enable/disable sounds in config."` + - **Success Criteria**: Sounds play when enabled, silent when disabled + +--- + +## Phase 4: Packaging & Distribution + +### 4.1 Create pyproject.toml +- [ ] Use Tau to create proper Python packaging + - **Prompt**: `"Create a pyproject.toml for the project with proper metadata, dependencies, and entry points."` + - **Success Criteria**: `pip install -e .` works + +### 4.2 Generate Desktop Entry +- [ ] Use Tau to improve desktop integration + - **Prompt**: `"Improve the desktop entry file to be more robust with proper icons and categories."` + - **Success Criteria**: Desktop entry works on GNOME/KDE + +### 4.3 Create Systemd Service +- [ ] Use Tau to create auto-start service + - **Prompt**: `"Create a systemd user service file so the application starts automatically on login."` + - **Success Criteria**: Service can be enabled/disabled with standard systemctl commands + +--- + +## Phase 5: Documentation + +### 5.1 User Documentation +- [ ] Use Tau to write user docs + - **Prompt**: `"Write a comprehensive user guide explaining how to install, configure, and use the application."` + - **Success Criteria**: Clear, complete documentation for end users + +### 5.2 Developer Documentation +- [ ] Use Tau to write dev docs + - **Prompt**: `"Write a development guide explaining the code structure, how to contribute, and how to test."` + - **Success Criteria**: New contributors can understand the project + +### 5.3 Man Page +- [ ] Use Tau to generate man page + - **Prompt**: `"Generate a man page from the CLI help and documentation."` + - **Success Criteria**: `man ` works (after installation) + +--- + +## Phase 6: CI/CD Pipeline + +### 6.1 GitHub Actions Workflow +- [ ] Use Tau to create CI pipeline + - **Prompt**: `"Create a GitHub Actions workflow that runs tests on push and PR, checks typing with mypy, and lints with ruff."` + - **Success Criteria**: Workflow file is valid and would pass + +### 6.2 Release Automation +- [ ] Use Tau to add release automation + - **Prompt**: `"Add a GitHub Actions workflow for publishing releases when tags are pushed."` + - **Success Criteria**: Workflow handles version detection and upload + +--- + +## 📊 Progress Tracking + +- **Total Milestones**: 19 +- **Completed**: 2 +- **In Progress**: 0 +- **Remaining**: 17 +- **Progress**: 10.53% + +--- + +## 🔄 Workflow + +For each milestone: + +1. **Read** the milestone description and prompt from this file +2. **Invoke Tau** with the specified prompt (or adapted version) +3. **Review** the output - does it meet success criteria? +4. **Document** results in the journal: + - Add entries under the milestone with `**Journal**`, `**Time**`, `**Blockers**`, `**Decision**`, `**Tau Notes**`, or `**Lesson**` +5. **Mark complete** by changing `[ ]` to `[x]` when done +6. **Iterate** - if Tau failed, adjust the prompt or break into smaller steps + +--- + +## 📝 Notes + +- If Tau cannot complete a task, document **WHY** in a journal entry under that milestone +- If a task reveals a Tau limitation, document it as a `**Tau Notes**` entry AND in the knowledge base +- The goal is to understand what Tau can and cannot do, not to "finish" the roadmap +- Quality of Tau's output matters more than speed of completion +- Use `/roadmap` commands to interact with this file + +--- + +## 🎯 Next Steps + +**Foundation skills are complete!** Start with milestone **1.1 Analyze Current Architecture**: +``` +tau -p "Analyze the project codebase and suggest a better project structure. Do not implement yet." +``` + +Or use the roadmap skill: +``` +/roadmap next # Shows 1.1 +/roadmap start 1.1 # Starts timer +/roadmap journal 1.1 "message" # Add notes +/roadmap complete 1.1 # Mark done +``` + +**Note**: The test-code skill (0.1) and web-search skill (0.2) are now available for all milestones. diff --git a/peak-monitor.desktop b/peak-monitor.desktop new file mode 100644 index 0000000..b83a3ae --- /dev/null +++ b/peak-monitor.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Type=Application +Name=Peak Monitor +Comment=Monitors peak hours (09:00-12:00, 14:00-18:00) Atlantic/Canary +Exec=python3 /home/ijuanes/CodigoFuente/Timer/peak_monitor.py +Terminal=false +Icon=/tmp/peak_monitor_46_204_113.png +Categories=Utility; +X-GNOME-Autostart-enabled=true diff --git a/peak_monitor.py b/peak_monitor.py new file mode 100644 index 0000000..cb333e2 --- /dev/null +++ b/peak_monitor.py @@ -0,0 +1,289 @@ +#!/usr/bin/env python3 +""" +Peak / Valley Time Monitor — system tray app for Linux. +Monitors two daily peak periods (Atlantic/Canary local time): + Morning: 09:00 – 12:00 + Afternoon: 14:00 – 18:00 + +Sends a desktop notification when each period starts and ends. +Left-click the tray icon to see time remaining in the current block +(or time until the next peak block). + +Requires: python3, python3-gi, gir1.2-gtk-3.0, gir1.2-ayatanaappindicator3-0.1, + python3-pil, libnotify-bin (notify-send) +""" + +import datetime +import os +import subprocess +import sys +from zoneinfo import ZoneInfo + +import gi +gi.require_version("Gtk", "3.0") +gi.require_version("AyatanaAppIndicator3", "0.1") +from gi.repository import Gtk, GLib, AyatanaAppIndicator3 + +from PIL import Image, ImageDraw + +# --------------------------------------------------------------------------- +# Configuration +# --------------------------------------------------------------------------- + +TZ = ZoneInfo("Atlantic/Canary") + +PEAKS = [ + ("Morning Peak", datetime.time(9, 0), datetime.time(12, 0)), + ("Afternoon Peak", datetime.time(14, 0), datetime.time(18, 0)), +] + +ICON_SIZE = 48 # px (system tray usually scales this down) +POLL_SECONDS = 15 # how often we re-check the clock +NOTIFY_WINDOW = datetime.timedelta(minutes=1) # grace window to send boundary notification + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def _now() -> datetime.datetime: + return datetime.datetime.now(TZ) + + +def _make_icon_png(rgb: tuple[int, int, int]) -> str: + """Create a plain coloured circle PNG in /tmp and return its path.""" + path = f"/tmp/peak_monitor_{rgb[0]}_{rgb[1]}_{rgb[2]}.png" + if os.path.exists(path): + return path # reuse + + img = Image.new("RGBA", (ICON_SIZE, ICON_SIZE), (0, 0, 0, 0)) + draw = ImageDraw.Draw(img) + m = 4 # margin so the circle isn't clipped + draw.ellipse([m, m, ICON_SIZE - m, ICON_SIZE - m], fill=rgb) + img.save(path, "PNG") + return path + + +def _notify(title: str, body: str) -> None: + """Fire a desktop notification via notify-send.""" + subprocess.run( + ["notify-send", title, body], + capture_output=True, + check=False, + ) + + +# --------------------------------------------------------------------------- +# Core logic +# --------------------------------------------------------------------------- + +def _current_peak(t: datetime.time) -> tuple[bool, tuple | None]: + """Return (is_peak, matching_peak_tuple_or_None).""" + for peak in PEAKS: + if peak[1] <= t < peak[2]: + return True, peak + return False, None + + +def _next_peak_start(now_dt: datetime.datetime) -> datetime.datetime | None: + """Return the next peak start datetime (today or tomorrow), or None.""" + today = now_dt.date() + candidates = [] + for peak in PEAKS: + start_dt = datetime.datetime.combine(today, peak[1], tzinfo=TZ) + candidates.append(start_dt) + # also tomorrow's start (in case all today's starts have passed) + candidates.append(start_dt + datetime.timedelta(days=1)) + future = [c for c in candidates if c > now_dt] + return min(future) if future else None + + +def _format_remaining(delta: datetime.timedelta) -> str: + """Human-friendly timedelta string.""" + total_min = int(delta.total_seconds() / 60) + if total_min <= 0: + return "now" + h, m = divmod(total_min, 60) + if h and m: + return f"{h}h {m}m" + if h: + return f"{h}h" + return f"{m}m" + + +# --------------------------------------------------------------------------- +# GTK + AppIndicator application +# --------------------------------------------------------------------------- + +class PeakMonitor: + def __init__(self) -> None: + # -- icons ---------------------------------------------------------- + self.icon_green = _make_icon_png((46, 204, 113)) # peak + self.icon_gray = _make_icon_png((149, 165, 166)) # valley + + # -- notification dedup --------------------------------------------- + # Keys like "2025-06-15:morning_start"; we clear stale keys on date roll. + self._notified: set[str] = set() + self._last_date: datetime.date | None = None + + # -- indicator (mute C-library deprecation warning) ----------------- + _saved = os.dup(2) + os.close(2) + os.open(os.devnull, os.O_WRONLY) + try: + self.indicator = AyatanaAppIndicator3.Indicator.new( + "peak-monitor", + self.icon_gray, + AyatanaAppIndicator3.IndicatorCategory.APPLICATION_STATUS, + ) + finally: + os.dup2(_saved, 2) + os.close(_saved) + + self.indicator.set_status(AyatanaAppIndicator3.IndicatorStatus.ACTIVE) + + # -- menu ----------------------------------------------------------- + self.menu = Gtk.Menu() + + self.status_item = Gtk.MenuItem(label="Starting…") + self.status_item.connect("activate", self._on_status_click) + self.menu.append(self.status_item) + + self.menu.append(Gtk.SeparatorMenuItem()) + + quit_item = Gtk.MenuItem(label="Quit") + quit_item.connect("activate", self._on_quit) + self.menu.append(quit_item) + + self.menu.show_all() + self.indicator.set_menu(self.menu) + + # -- periodic timer ------------------------------------------------- + GLib.idle_add(self._tick) # first tick immediately + GLib.timeout_add_seconds(POLL_SECONDS, self._tick) + + # ------------------------------------------------------------------ update + + def _tick(self) -> bool: + current = _now() + cur_time = current.time() + cur_date = current.date() + + # --- roll daily notification dedup set --------------------------- + if self._last_date != cur_date: + self._notified.clear() + self._last_date = cur_date + + in_peak, peak = _current_peak(cur_time) + + # --- icon & tooltip ---------------------------------------------- + if in_peak: + name, start, end = peak + end_dt = datetime.datetime.combine(cur_date, end, tzinfo=TZ) + remaining = end_dt - current + tooltip = f"☀ {name} — {_format_remaining(remaining)} left" + self.indicator.set_icon_full(self.icon_green, tooltip) + self.status_item.set_label(f"{name}: {_format_remaining(remaining)} left") + else: + next_start = _next_peak_start(current) + if next_start: + till = _format_remaining(next_start - current) + tooltip = f"🌙 Valley — next peak in {till}" + self.status_item.set_label(f"Valley — next peak in {till}") + else: + tooltip = "🌙 Valley" + self.status_item.set_label("Valley") + self.indicator.set_icon_full(self.icon_gray, tooltip) + + # --- boundary notifications -------------------------------------- + self._check_boundaries(current, cur_time, cur_date, in_peak, peak) + + return True # keep GLib timeout alive + + # ------------------------------------------------------------------ boundaries + + def _check_boundaries( + self, + now_dt: datetime.datetime, + now_t: datetime.time, + today: datetime.date, + in_peak: bool, + peak: tuple | None, + ) -> None: + """Fire notify-send at period start/end if within the grace window.""" + for name, start_t, end_t in PEAKS: + # --- period start ---------------------------------------------- + key_start = f"{today.isoformat()}:{name}:start" + start_dt = datetime.datetime.combine(today, start_t, tzinfo=TZ) + if ( + abs(now_dt - start_dt) <= NOTIFY_WINDOW + and key_start not in self._notified + ): + _notify( + f"⏰ {name} started", + f"{start_t.strftime('%H:%M')} – {end_t.strftime('%H:%M')} " + f"(Atlantic/Canary)", + ) + self._notified.add(key_start) + + # --- period end ------------------------------------------------ + key_end = f"{today.isoformat()}:{name}:end" + end_dt = datetime.datetime.combine(today, end_t, tzinfo=TZ) + if ( + abs(now_dt - end_dt) <= NOTIFY_WINDOW + and key_end not in self._notified + ): + _notify( + f"⏰ {name} ended", + f"Peak period {start_t.strftime('%H:%M')} – " + f"{end_t.strftime('%H:%M')} is over.", + ) + self._notified.add(key_end) + + # ------------------------------------------------------------------ actions + + def _on_status_click(self, _widget) -> None: + """Left-click on the status menu item: show a detailed notification.""" + current = _now() + cur_time = current.time() + in_peak, peak = _current_peak(cur_time) + + if in_peak: + name, start, end = peak + end_dt = datetime.datetime.combine(current.date(), end, tzinfo=TZ) + left = _format_remaining(end_dt - current) + _notify( + f"☀ Currently in {name}", + f"Started {start.strftime('%H:%M')}, " + f"ends {end.strftime('%H:%M')}\n" + f"⏳ {left} remaining", + ) + else: + next_start = _next_peak_start(current) + if next_start: + till = _format_remaining(next_start - current) + ns_name = next_start.strftime("%A %H:%M") + _notify( + "🌙 Valley (off-peak)", + f"Next peak starts {ns_name}\n⏳ {till} until then", + ) + else: + _notify("🌙 Valley", "No upcoming peak periods.") + + def _on_quit(self, _widget) -> None: + Gtk.main_quit() + + +# --------------------------------------------------------------------------- +# Entry point +# --------------------------------------------------------------------------- + +def main() -> None: + PeakMonitor() + try: + Gtk.main() + except KeyboardInterrupt: + pass + + +if __name__ == "__main__": + main()