I often have cause to plan aspects of events/work in my life as much as a year out. Consequently, I would like to generate a yearly org-agenda
view which has the following properties:
- Shows not the current year from start to finish, but the span of one year beginning with the current date up to the equivalent date the following year.
- Items scheduled/deadlined/dated are broken out into groups by month, with each group labeled with the name of the month.
- Dates that have no items do not appear on the agenda at all.
- Items that I enter using
org-anniversary
such as birthdays appear.
The requirement of grouping, coupled with the fact that I already use org-super-agenda
led me to try applying it to this case. I've written some small helper functions that can produce dates shifted from today by day or year, and can produce the first and last day of shifted months.
(defun years-from-today (&optional num)
"Return this day on this month NUM years from now as time.
If NUM is nil, return this day on this month next year as time."
(let* ((num (or num 1))
(now (decode-time (current-time)))
(day (nth 3 now))
(month (nth 4 now))
(year (+ num (nth 5 now))))
(encode-time 0 0 0 day month year)))
(defun first-day-of-month (&optional num)
"Return the first day of the month NUM months from now as time.
If NUM is nil, return the first day of this month as time."
(let* ((num (or num 0))
(now (decode-time (current-time)))
(month (+ num (nth 4 now)))
(year (nth 5 now)))
(encode-time 0 0 0 1 month year)))
(defun last-day-of-month (&optional num)
"Return the last day of the month NUM months from now as time.
If NUM is nil, return the last day of this month as time."
(let* ((num (or num 0))
(now (decode-time (current-time)))
(month (+ num (nth 4 now)))
(year (nth 5 now))
(last-day-of-month (calendar-last-day-of-month month year)))
(encode-time 0 0 0 last-day-of-month month year)))
(defun days-from-today (&optional num)
"Return the date NUM days from now. If NUM is nil,
return this day on this month next year as time."
(let* ((num (or num 0))
(now (decode-time (current-time)))
(day (+ num (nth 3 now)))
(month (nth 4 now))
(year (nth 5 now)))
(encode-time 0 0 0 day month year)))
Passing the result of any of these to (format-time-string "%Y-%m-%d" (func))
will yield date strings in the format org-super-agenda
requires. The idea was to use these functions to supply date ranges to the :scheduled
or :deadlined
selectors in org-super-agenda-groups
on a view of type agenda
. I set the variable org-agenda-show-all-dates
to nil
and I write the following as a proof-of-concept for just the current month:
(setq org-agenda-custom-commands
'(("u" "Upcoming Year"
((agenda "" ((org-agenda-span 'year)
(org-agenda-show-all-dates nil)
(org-super-agenda-groups
'((:name "This Month"
:time-grid f
:and (:scheduled (format-time-string "%Y-%m-%d" (days-from-today))
:scheduled (format-time-string "%Y-%m-%d" (last-day-of-month))))))))))))
What I end up getting is the following:
Year-agenda:
Friday 5 July 2024
Other items
inbox: 8 d. ago: TODO Read Reading List Item
inbox: TODO Habit !
Monday 8 July 2024
Other items
agenda: A birthday
Tuesday 9 July 2024 W28
Other items
inbox: Scheduled: TODO An Emacs config task
Wednesday 24 July 2024
Other items
inbox: Scheduled: TODO Configure btop
Sunday 11 August 2024
Other items
agenda: Another birthday
Tuesday 5 November 2024
Other items
agenda: Another birthday
Saturday 7 December 2024
Other items
agenda: Another birthday
Wednesday 22 January 2025
Other items
agenda: Another birthday
Friday 11 April 2025
Other items
agenda: Another birthday
Sunday 4 May 2025
Other items
agenda: Another birthday
Because things are showing up under the Other items
heading even when they are in this month, and the This Month
heading doesn't appear anywhere, I suspect this means the group isn't catching these things at all, and this is all just stuff falling through to the default. To check, I modify the above custom command like so:
(setq org-agenda-custom-commands
'(("u" "Upcoming Year"
((agenda "" ((org-agenda-span 'year)
(org-agenda-show-all-dates nil)
(org-super-agenda-groups
'((:name "This Month"
:time-grid f
:and (:scheduled (after (format-time-string "%Y-%m-%d" (days-from-today)))
:scheduled (before (format-time-string "%Y-%m-%d" (last-day-of-month)))))
(:discard (:anything t))))))))))
And now the agenda just looks like this:
Year-agenda:
Saturday 6 July 2024
Monday 8 July 2024 W28
This Month
inbox: Scheduled: TODO Emacs config task
Wednesday 24 July 2024
This Month
inbox: Scheduled: TODO Configure btop
Sunday 11 August 2024
Tuesday 5 November 2024
Saturday 7 December 2024
Wednesday 22 January 2025
Friday 11 April 2025
Sunday 4 May 2025
NB: The above result is from Saturday, July 6th when the edits were made, as opposed to the earlier content from Friday, July 5th.
So we see the present day, though it has no scheduled items. We also see rows for each upcoming day that has something scheduled, though unsurprisingly the org-anniversary
items do not show up as they do not have scheduled time stamps. Additionally, the group name shows up now, but it shows up under the heading for the date, for each date that has an entry. This is the inverse of what I want: the "This Month" heading to appear once, and then have all the items grouped under it. I also don't want the date to be a heading above its entries, but to appear to one side for all entries.