diff --git a/doc/source/usage.rst b/doc/source/usage.rst index c07ddf390..0d6ec29cb 100644 --- a/doc/source/usage.rst +++ b/doc/source/usage.rst @@ -115,6 +115,9 @@ Several options are common to almost all of :program:`khal`'s commands alarm-symbol An alarm symbol (alarm clock) if the event has at least one alarm. + alarms-list + A comma-separated list of alarms for the event (e.g., `alarm1@-15m, getready@-1h`). + location The event location. @@ -222,7 +225,7 @@ Several options are common to almost all of :program:`khal`'s commands end-date-long, end-time, start-full, start-long-full, start-date-full, start-date-long-full, start-time-full, end-full, end-long-full, end-date-full, end-date-long-full, - end-time-full, repeat-symbol, location, calendar, + end-time-full, repeat-symbol, alarms-list, location, calendar, calendar-color, start-style, to-style, end-style, start-end-time-style, end-necessary, end-necessary-long, status, cancelled, organizer, url, duration, duration-full, diff --git a/khal/khalendar/event.py b/khal/khalendar/event.py index d4bca6598..f660b4d44 100644 --- a/khal/khalendar/event.py +++ b/khal/khalendar/event.py @@ -26,6 +26,7 @@ import logging import os from collections.abc import Callable +from typing import Any import icalendar import icalendar.cal @@ -612,7 +613,7 @@ def attributes( """ env = env or {} - attributes = {} + attributes: dict[str, Any] = {} if isinstance(relative_to, tuple): relative_to_start, relative_to_end = relative_to else: @@ -734,6 +735,14 @@ def attributes( attributes["repeat-symbol"] = self._recur_str attributes["repeat-pattern"] = self.recurpattern attributes["alarm-symbol"] = self._alarm_str + attributes["alarms-list"] = [ + { + "delta": alarm[0].total_seconds(), + "description": str(alarm[1]), + "delta-formatted": timedelta2str(alarm[0]), + } + for alarm in self.alarms + ] attributes["status-symbol"] = self._status_str attributes["partstat-symbol"] = self._partstat_str attributes["title"] = self.summary diff --git a/khal/utils.py b/khal/utils.py index 1a8af49cd..46394d6b9 100644 --- a/khal/utils.py +++ b/khal/utils.py @@ -197,6 +197,12 @@ def fmt(rows): if "calendar-color" in row: row["calendar-color"] = get_color(row["calendar-color"]) + if "alarms-list" in row and isinstance(row["alarms-list"], list): + row["alarms-list"] = ", ".join( + alarm["description"] + "@" + alarm["delta-formatted"] + for alarm in row["alarms-list"] + ) + s = format_string.format(**row) if colors: @@ -245,6 +251,7 @@ def fmt(rows): "end-necessary-long", "repeat-symbol", "repeat-pattern", + "alarms-list", "title", "organizer", "description", diff --git a/tests/cli_test.py b/tests/cli_test.py index 6fbbc238b..7d6a8441e 100644 --- a/tests/cli_test.py +++ b/tests/cli_test.py @@ -527,6 +527,32 @@ def test_list_json(runner): assert result.output.startswith(expected) +def test_list_alarms(runner): + runner = runner(days=2) + now = dt.datetime.now().strftime("%d.%m.%Y") + runner.invoke(main_khal, f"new {now} 18:00 myevent --alarms -15m,1h".split()) + args = ["list", "--format", "{alarms-list}", "--day-format", ""] + result = runner.invoke(main_khal, args) + assert not result.exception + assert result.output.strip() == "@15m, @-1h" + + +def test_list_alarms_json(runner): + runner = runner() + now = dt.datetime.now().strftime("%d.%m.%Y") + runner.invoke(main_khal, f"new {now} 18:00 myevent --alarms 15m,1h".split()) + args = ["list", "--json", "alarms-list"] + result = runner.invoke(main_khal, args) + expected = '[{"alarms-list": [\ +{"delta": -900.0, "description": "", "delta-formatted": "-15m"}, \ +{"delta": -3600.0, "description": "", "delta-formatted": "-1h"}\ +]}]' + print(result.output) + print(expected) + assert not result.exception + assert result.output.startswith(expected) + + def test_search(runner): runner = runner(days=2) now = dt.datetime.now().strftime("%d.%m.%Y") diff --git a/tests/event_test.py b/tests/event_test.py index 4cb7073b5..c33e4ed94 100644 --- a/tests/event_test.py +++ b/tests/event_test.py @@ -596,6 +596,28 @@ def test_event_alarm(): assert event.alarms == [(dt.timedelta(-1, 82800), vText("new event"))] +def test_event_alarm_list(): + """test the content of `alarms-list` attribute""" + event = Event.fromString(_get_text("event_dt_simple"), **EVENT_KWARGS) + assert event.alarms == [] + event.update_alarms( + [(dt.timedelta(minutes=30), "alarm 1"), (-dt.timedelta(hours=1, minutes=30), "alarm 2")] + ) + attributes = event.attributes(dt.date.today()) + assert attributes["alarms-list"] == [ + {"delta": 1800.0, "description": "alarm 1", "delta-formatted": "30m"}, + {"delta": -5400.0, "description": "alarm 2", "delta-formatted": "-1h -30m"}, + ] + + +def test_event_no_alarms_list(): + """test that `alarms-list` is empty for an event with no alarms""" + event = Event.fromString(_get_text("event_dt_simple"), **EVENT_KWARGS) + assert event.alarms == [] + attributes = event.attributes(dt.date.today()) + assert attributes["alarms-list"] == [] + + def test_event_attendees(): event = Event.fromString(_get_text("event_dt_simple"), **EVENT_KWARGS) assert event.attendees == ""