UP | HOME

▼ 本文更新于 [2025-09-09 二 20:20]

emacs-在org-agenda中展示年度、季度、月度、周度任务

经常使用任务管理软件的朋友都知道,我们有的任务并无具体的截止日期,而是一个笼统的区间,比如年度、季度、月度、周度任务。org-mode中基于 SCHEDULEDDEADLINE 的传统方式,难以满足这种任务的查看与重复需求。

经过一番思索,我找到了利用Org-QLorg-repeat-by-cron实现相关任务查看重复的方法,分享与此。

提示:org-repeat-by-cron 是我修改自 https://github.com/Raemi/org-reschedule-by-rule 的包,略微修改之处在此按下不表。

1. 设置需要被追踪的任务

在需要被追踪的任务中,添加名为 PERIOD 的属性。

由于Emacs Org-Mode 重复任务在按月重复情况下无法定位于「某月的最后一天」,因此后面的季度、月度任务需要用到 cron 来帮助重复定位。

* TODO 这是一个年度任务
DEADLINE: <2025-12-31 周三 ++1y>
:PROPERTIES:
:PERIOD:   year
:END:

* TODO 这是一个季度任务
DEADLINE: <2025-09-30 周二>
:PROPERTIES:
:REPEAT_CRON: L 3,6,9,12 *
:PERIOD:   season
:REPEAT_DEADLINE: t
:END:

* TODO 这是一个月度任务
DEADLINE: <2025-09-30 周二>
:PROPERTIES:
:REPEAT_CRON: L * *
:PERIOD:   month
:REPEAT_DEADLINE: t
:END:

* TODO 这是一个周度任务
DEADLINE: <2025-09-12 周五 ++1w>
:PROPERTIES:
:PERIOD:   week
:END:

2. 设置Agenda Commands

2.1. 自定义相关函数

2.1.1. my/org-ql-ts-period

这里我们定义了两个相关函数。第一个函数 my/org-ql-ts-period 会根据输入的周期,返回当前时间所在周期的起止日期。

如在2025年9月9日执行 (my/org-ql-ts-period 's) ,则会返回 ("2025-07-01" . "2025-09-30")

(defun my/org-ql-ts-period (period)
"根据输入的 PERIOD ('y', 's', 'm', 'w') 返回当前年、季度、月或周的起止日期。
格式为 'YYYY-MM-DD'。

季节 (Season) 定义:
- 季度1: 1月 - 3月
- 季度2: 4月 - 6月
- 季度3: 7月 - 9月
- 季度4: 10月 - 12月

星期一被视为一周的开始。

参数:
PERIOD: 一个表示时间段的符号,可以是 'y', 's', 'm', 或 'w'。

返回:
一个包含起止日期的字符串,格式为 'YYYY-MM-DD - YYYY-MM-DD',
如果输入无效则返回错误信息。"
  (let* ((now (current-time))
         (decoded-time (decode-time now))
         (sec (nth 0 decoded-time))
         (min (nth 1 decoded-time))
         (hour (nth 2 decoded-time))
         (day (nth 3 decoded-time))
         (month (nth 4 decoded-time))
         (year (nth 5 decoded-time))
         start-date
         end-date)
    (cond
     ;; 年 (Year)
     ((eq period 'y)
      (setq start-date (format-time-string "%Y-01-01"))
      (setq end-date (format-time-string "%Y-12-31")))

     ;; 季 (Season)
     ((eq period 's)
      (let* ((start-month (cond ((<= month 3) 1)
                                ((<= month 6) 4)
                                ((<= month 9) 7)
                                (t 10)))
             (end-month (+ start-month 2))
             (end-day (calendar-last-day-of-month end-month year)))
        (setq start-date (format-time-string "%Y-%m-01" (encode-time 0 0 0 1 start-month year)))
        (setq end-date (format-time-string (format "%%Y-%%m-%d" end-day) (encode-time 0 0 0 end-day end-month year)))))

     ;; 月 (Month)
     ((eq period 'm)
      (setq start-date (format-time-string "%Y-%m-01"))
      (let* ((last-day (calendar-last-day-of-month month year)))
        (setq end-date (format-time-string (format "%%Y-%%m-%d" last-day)))))

     ;; 周 (Week)
     ((eq period 'w)
      (let* ((day-of-week (string-to-number (format-time-string "%u"))) ; 星期一为1,星期日为7
             (start-offset (- day-of-week 1))
             (end-offset (- 7 day-of-week))
             (start-time (time-subtract now (seconds-to-time (* start-offset 24 60 60))))
             (end-time (time-add now (seconds-to-time (* end-offset 24 60 60)))))
        (setq start-date (format-time-string "%Y-%m-%d" start-time))
        (setq end-date (format-time-string "%Y-%m-%d" end-time))))

     ;; 无效输入
     (t (error "无效的参数,请输入 'y', 's', 'm', 或 'w'")))

    (cons start-date end-date)))

2.1.2. my/org-ql-block-period

第二个函数 my/org-ql-block-period 用来简化 org-agenda-custom-commands 中可能出现的重复代码。根据输入的周期,构建不同的 org-ql-block

(defun my/org-ql-block-period (period)
  (let ((property-string nil)
        (header-string nil))
    (cond
     ;; 年 (Year)
     ((eq period 'y)
      (setq property-string "year"
            header-string "年"))
     ;; 季 (Season)
     ((eq period 's)
      (setq property-string "season"
            header-string "季"))
     ;; 月 (Month)
     ((eq period 'm)
      (setq property-string "month"
            header-string "月"))
     ;; 周 (Week)
     ((eq period 'w)
      (setq property-string "week"
            header-string "周"))
     ;; 无效输入
     (t (error "无效的参数,请输入 'y', 's', 'm', 或 'w'")))
    (org-ql-block `(and (todo "TODO" "HOLD") (property "PERIOD" property-string)(ts-active :from ,(car (my/org-ql-ts-period period)) :to ,(cdr (my/org-ql-ts-period period))))
                    ((org-ql-block-header ,(concat "🔄周期--" header-string "🔄"))))))
;; 参考设置
;; (my/org-ql-block-period 'y)

2.1.3. org-agenda-custom-commands

最后,我们需要自定义 org-agenda-custom-commands ,加入上述块函数:

(setq org-agenda-custom-commands
      '(("d" "Daily Agenda"
         ;; 上略……
         ;; 周期任务拿出来
         (my/org-ql-block-period 'y)
         (my/org-ql-block-period 's)
         (my/org-ql-block-period 'm)
         (my/org-ql-block-period 'w)
          ;; 下略…
          )))

这样一来,执行 (org-agenda nil "d") 之后,就能在Agenda 区域中看见

──────────────────────────────────────────────────────────────
🔄周期--年🔄
  TODO 这是一个年度任务  

───────────────────────────────────────────────────────────────
🔄周期--季🔄
  TODO 这是一个季度任务 

───────────────────────────────────────────────────────────────
🔄周期--月🔄
  TODO 这是一个月度任务  

───────────────────────────────────────────────────────────────
🔄周期--周🔄
  TODO 这是一个周度任务  

3. 原理解释

org-ql-block 函数可以将搜索结果展示为agenda中的一个block。这里我们搜索的内容为 (and (todo "TODO" "HOLD") (property "PERIOD" property-string)(ts-active :from ,(car (my/org-ql-ts-period period)) :to ,(cdr (my/org-ql-ts-period period)))) ,其实是满足以下三个要求的任务:

  • (todo "TODO" "HOLD") :TODO或者HOLD这种处于TODO状态的HEADING
  • (property "PERIOD" property-string) :根据上面的要求,搜索 PERIOD 属性为 year season month week 的对应任务
  • (ts-active :from ,(car (my/org-ql-ts-period period)) :to ,(cdr (my/org-ql-ts-period period))) :活跃时间戳,且时间为 year season month week 对应的当前日期所处起始年月日里。由 my/org-ql-ts-period 函数生成。

这里我们用 DEADLINE 标记,就是为了腾出空间留给 SCHEDULED ,以便我们计划这类任务应该开始完成的时间。如果一个任务被标记完成,那么它会自动标记 DEADLINE 在符合条件的下个周期最后一天,也就会从我们统计的Agenda-View里移除了(因为时间戳超过了期限)。

在org-todo标记为完成后,因为我们用的 DEADLINE ,会将 SCHEDULED 清空,并重新设定 DEADLINE 时间戳。

© Published by Emacs 31.0.50 (Org mode 9.8-pre) | RSS 评论