Sunrise and sunset in Waybar with astral and uv
I wanted the next sunrise and sunset times in my waybar -- partly out of curiosity, partly because I cycle a lot and want to know how much daylight I still have. The astral Python library does the math; uv runs it without me having to manage a virtualenv.

The script
The interesting part is that the Python script is a PEP 723 single-file script. Its dependencies live in a comment block at the top, and the shebang tells uv to handle the rest:
#!/usr/bin/env -S uv run --script # /// script # requires-python = ">=3.11" # dependencies = ["astral"] # ///
When you chmod +x it and run it, uv reads the metadata, builds (or reuses) a cached environment under ~/.cache/uv/environments-v2/, and executes the script in it.
Nothing to install globally, no requirements.txt, no project venv to ship.
Astral has a built-in geocoder, but only one of the German cities I cared about (Berlin) is in its database. So I hardcoded a small lat/lon table for the cities I look up:
CITIES: dict[str, tuple[float, float]] = { "Stuttgart": (48.7758, 9.1829), "Karlsruhe": (49.0069, 8.4037), "Berlin": (52.5200, 13.4050), "Munich": (48.1351, 11.5820), }
The render function picks the next upcoming sunrise and the next upcoming sunset (today's if still ahead, tomorrow's otherwise) and orders them so that whichever happens sooner is shown first:
def render(city: str, now: datetime) -> str: lat, lon = CITIES[city] loc = LocationInfo(city, "Germany", "Europe/Berlin", lat, lon) today = sun(loc.observer, date=now.date(), tzinfo=TZ) tomorrow = sun(loc.observer, date=now.date() + timedelta(days=1), tzinfo=TZ) rise = today["sunrise"] if now < today["sunrise"] else tomorrow["sunrise"] set_ = today["sunset"] if now < today["sunset"] else tomorrow["sunset"] if rise < set_: return f"\U0001F305 {rise:%H:%M} \U0001F307 {set_:%H:%M}" return f"\U0001F307 {set_:%H:%M} \U0001F305 {rise:%H:%M}"
So during daylight you see 🌇 20:48 🌅 05:50 (sunset is next, then tomorrow's sunrise), and at night or in the early morning hours you see 🌅 05:50 🌇 20:49.
Wiring it into waybar
A tiny shell wrapper makes the city configurable from the waybar config:
And in ~/.config/waybar/config:
The other use case
In waybar I only ever show Stuttgart -- but in the shell I look up other cities:
Useful before a cycling tour ("how late can I be on the road and still get back before dark?") or before an early start to catch a sunrise somewhere ("when do I actually have to get up?").
Tests
Because the rendering logic is a single pure function -- render(city, now) -> str -- the tests just pass a constructed datetime directly.
@pytest.mark.parametrize("city, now, expected", [ ("Stuttgart", datetime(2026, 5, 8, 3, 0, tzinfo=TZ), "🌅 05:52 🌇 20:48"), ("Stuttgart", datetime(2026, 5, 8, 14, 0, tzinfo=TZ), "🌇 20:48 🌅 05:50"), ("Stuttgart", datetime(2026, 5, 8, 22, 0, tzinfo=TZ), "🌅 05:50 🌇 20:49"), # ... ]) def test_render(city, now, expected): assert render(city, now) == expected
A pyproject.toml next to the script declares the dev dependencies, so uv run pytest works:
[project] name = "sun" version = "0.1.0" requires-python = ">=3.11" dependencies = ["astral"] [dependency-groups] dev = ["pytest"]
The script keeps its PEP 723 header, so waybar still calls it directly via the uv run --script shebang.
The project venv at ./.venv/ is only used for the tests; the waybar invocation uses the cached script env under ~/.cache/uv/.
Two independent environments for the same file -- both managed by uv, neither requiring me to type pip install.
