Hi Ihor,
Attached is the patch adding the subtree checkbox summary operators
("Xs", "Xs/", and "Xs%") to column view. I decided to keep the
COOKIE_DATA change separate and will follow up on that later to keep
this patch focused.
Best,
--
Slawomir Grochowski
>From 60c9933766b3fc769325b62b5e8dd755047e5415 Mon Sep 17 00:00:00 2001
From: Slawomir Grochowski <[email protected]>
Date: Sat, 5 Sep 2026 08:12:18 +0200
Subject: [PATCH] org-colview: Add subtree checkbox summary operators (Xs, Xs/,
Xs%)
* lisp/org-colview.el (org-columns-summary-types-default): Add
"Xs", "Xs/", and "Xs%" summary types.
(org-columns--checkbox-operators): Include subtree summary types.
(org-columns--checkbox-subtree-operators): New constant.
(org-columns--subtree-checkbox-summary-p): New function.
(org-columns--compute-spec): Pass subtree checkbox values up the tree
when operator is a subtree checkbox operator.
* doc/org-manual.org (Column attributes): Document "Xs", "Xs/", and
"Xs%" in the summary types table, and describe the difference between
children-only and subtree checkbox summary operators with a table example.
* etc/ORG-NEWS (New subtree checkbox summary operators in column view):
Announce the new summary types.
* testing/lisp/test-org-colview.el
(test-org-colview/columns-subtree-checkbox-statistics): New test.
---
doc/org-manual.org | 23 ++++++++++
etc/ORG-NEWS | 7 ++++
lisp/org-colview.el | 24 +++++++++--
testing/lisp/test-org-colview.el | 72 ++++++++++++++++++++++++++++++++
4 files changed, 123 insertions(+), 3 deletions(-)
diff --git a/doc/org-manual.org b/doc/org-manual.org
index 31d56594f..4d0b2f7d8 100644
--- a/doc/org-manual.org
+++ b/doc/org-manual.org
@@ -5789,6 +5789,9 @@ optional. The individual parts have the following meaning:
| =X= | Checkbox status: =[X]= if all children are =[X]=. |
| =X/= | Checkbox status: =[n/m]=. |
| =X%= | Checkbox status: =[n%]=. |
+ | =Xs= | Checkbox status: =[X]= if all checkboxes in subtree are =[X]=. |
+ | =Xs/= | Checkbox status: =[n/m]= for checkboxes in subtree. |
+ | =Xs%= | Checkbox status: =[n%]= for checkboxes in subtree. |
| =:= | Sum times (HH:MM); plain numbers are minutes. |
| =:min= | Smallest time value in column. |
| =:max= | Largest time value. |
@@ -5802,6 +5805,26 @@ optional. The individual parts have the following meaning:
You can also define custom summary types by setting
~org-columns-summary-types~.
+The checkbox summary types (=X=, =X/=, =X%= vs =Xs=, =Xs/=, =Xs%=)
+differ in how they aggregate deeper levels of the outline tree.
+The operators =X=, =X/=, and =X%= only summarize children: if a
+subheading has children of its own, it is treated as a single completed
+checkbox only when all of its children are complete. In contrast, the
+subtree operators =Xs=, =Xs/=, and =Xs%= count all checkboxes across
+the entire subtree.
+
+For example, given a column displaying checkbox status:
+
+| Outline | ={X/}= | ={Xs/}= |
+|--------------+---------+---------|
+| =* Top= | =[1/2]= | =[3/4]= |
+| =** Sub 1= | =[1/2]= | =[1/2]= |
+| =*** Task 1= | =[X]= | =[X]= |
+| =*** Task 2= | =[ ]= | =[ ]= |
+| =** Sub 2= | =[2/2]= | =[2/2]= |
+| =*** Task 3= | =[X]= | =[X]= |
+| =*** Task 4= | =[X]= | =[X]= |
+
The =est+= summary type requires further explanation. It is used for
combining estimates, expressed as low-high ranges. For example,
instead of estimating that a particular task will take 5 days, you might
diff --git a/etc/ORG-NEWS b/etc/ORG-NEWS
index 6b277eec7..a53ae9af7 100644
--- a/etc/ORG-NEWS
+++ b/etc/ORG-NEWS
@@ -78,6 +78,13 @@ to collect statistics across the entire subtree instead of only children.
The previous keyword =recursive= remains supported for backward
compatibility.
+*** New subtree checkbox summary operators in column view
+
+Column view now supports subtree checkbox summary types: =Xs=,
+=Xs/=, and =Xs%=. Unlike their children-only counterparts (=X=,
+=X/=, =X%=), which only summarize children, these operators
+count all checkboxes across the entire subtree.
+
*** Link targets can be used as search option in =id:= links
When storing an =id:= link to target, the target will now be used as a
diff --git a/lisp/org-colview.el b/lisp/org-colview.el
index 290b9b3dc..65ddf3822 100644
--- a/lisp/org-colview.el
+++ b/lisp/org-colview.el
@@ -141,6 +141,9 @@ For more information, see `org-columns-dblock-write-default'."
("X" . org-columns--summary-checkbox)
("X/" . org-columns--summary-checkbox-count)
("X%" . org-columns--summary-checkbox-percent)
+ ("Xs" . org-columns--summary-checkbox)
+ ("Xs/" . org-columns--summary-checkbox-count)
+ ("Xs%" . org-columns--summary-checkbox-percent)
("max" . org-columns--summary-max)
("mean" . org-columns--summary-mean)
("min" . org-columns--summary-min)
@@ -155,6 +158,13 @@ For more information, see `org-columns-dblock-write-default'."
"Map operators to summary functions.
See `org-columns-summary-types' for details.")
+(defconst org-columns--checkbox-subtree-operators '("Xs" "Xs/" "Xs%")
+ "Summary operators computing a checkbox summary of a whole subtree.")
+
+(defconst org-columns--checkbox-operators
+ (append '("X" "X/" "X%") org-columns--checkbox-subtree-operators)
+ "Summary operators computing a checkbox summary.")
+
;;;; Column View
@@ -993,7 +1003,7 @@ an integer, select that value."
(or (org-property-get-allowed-values pom key)
(and (member (org-columns--spec-operator
(org-columns--spec-at-point))
- '("X" "X/" "X%"))
+ org-columns--checkbox-operators)
org-columns-checkbox-allowed-values)
(org-colview-construct-allowed-dates value))))
(if previous (reverse all) all))))
@@ -1584,6 +1594,10 @@ they have their own way to be computed."
(and (not (member property org-special-properties))
(org-columns--spec-operator spec))))
+(defun org-columns--subtree-checkbox-summary-p (operator)
+ "Non-nil when OPERATOR counts the checkboxes of a whole subtree."
+ (member operator org-columns--checkbox-subtree-operators))
+
(defun org-columns--compute-spec (spec &optional update-property-p)
"Update tree according to SPEC.
SPEC is a column format specification. When optional argument
@@ -1594,6 +1608,7 @@ existing ones in properties drawers."
(format-string (org-columns--spec-format-string spec))
(collect-function (org-columns--collect-function operator))
(summarize-function (org-columns--summarize-function operator))
+ (subtree-checkboxes-p (org-columns--subtree-checkbox-summary-p operator))
(stack nil))
(org-with-wide-buffer
;; Walk the tree from the back and do the computations.
@@ -1618,8 +1633,11 @@ existing ones in properties drawers."
(when update-property-p (org-columns--update-summary-property property current-value summary))
summary)
(t current-value))))
- (when (org-string-nw-p value)
- (push (cons level value) stack)))))))))
+ (cond
+ ((and summary subtree-checkboxes-p)
+ (dolist (v (reverse child-values))
+ (push (cons level v) stack)))
+ ((org-string-nw-p value) (push (cons level value) stack))))))))))
;;;###autoload
(defun org-columns-compute (property)
diff --git a/testing/lisp/test-org-colview.el b/testing/lisp/test-org-colview.el
index ffd0d5cf9..05c66b42a 100644
--- a/testing/lisp/test-org-colview.el
+++ b/testing/lisp/test-org-colview.el
@@ -1122,6 +1122,78 @@ https://list.orgmode.org/[email protected]/T/#u."
(list (get-char-property (point) 'org-columns-value-modified)
(get-char-property (1+ (point)) 'org-columns-value-modified))))))
+(ert-deftest test-org-colview/columns-subtree-checkbox-statistics ()
+ "Test subtree checkbox summary types (Xs, Xs/, Xs%)."
+ ;; The tree has four leaf checkboxes (three checked, one unchecked).
+ ;; S1 has [1/2], S2 has [2/2].
+ (let ((tree "* H
+** S1
+*** S11
+:PROPERTIES:
+:A: [X]
+:END:
+*** S12
+:PROPERTIES:
+:A: [ ]
+:END:
+** S2
+*** S21
+:PROPERTIES:
+:A: [X]
+:END:
+*** S22
+:PROPERTIES:
+:A: [X]
+:END:"))
+ (cl-flet ((summary (format)
+ (org-test-with-temp-text tree
+ (let ((org-columns-default-format format))
+ (org-columns))
+ (get-char-property (point) 'org-columns-value-modified))))
+ ;; Non-recursive (X, X/, X%) only cover children:
+ ;; H has two children: S1 ([1/2], incomplete) and S2 ([2/2], complete).
+ (should (equal "[1/2]" (summary "%A{X/}")))
+ (should (equal "[50%]" (summary "%A{X%}")))
+ (should (equal "[-]" (summary "%A{X}")))
+ ;; Subtree variants (Xs/, Xs%, Xs) count all checkboxes in the entire subtree (3 of 4 checked).
+ (should (equal "[3/4]" (summary "%A{Xs/}")))
+ (should (equal "[75%]" (summary "%A{Xs%}")))
+ (should (equal "[-]" (summary "%A{Xs}")))))
+ ;; When all checkboxes in subtree are checked, Xs summarizes to [X].
+ (should
+ (equal "[X]"
+ (org-test-with-temp-text
+ "* H
+** S1
+*** S11
+:PROPERTIES:
+:A: [X]
+:END:
+*** S12
+:PROPERTIES:
+:A: [X]
+:END:"
+ (let ((org-columns-default-format "%A{Xs}"))
+ (org-columns))
+ (get-char-property (point) 'org-columns-value-modified))))
+ ;; When a subheading has its own checkbox property and also has children,
+ ;; the children's values override the parent's value so it is not counted twice.
+ (should
+ (equal "[0/1]"
+ (org-test-with-temp-text
+ "* H
+** S1
+:PROPERTIES:
+:A: [X]
+:END:
+*** S11
+:PROPERTIES:
+:A: [ ]
+:END:"
+ (let ((org-columns-default-format "%A{Xs/}"))
+ (org-columns))
+ (get-char-property (point) 'org-columns-value-modified)))))
+
(ert-deftest test-org-colview/columns-custom-summary-hierarchical ()
"Test custom summary types on hierarchical headings."
(should
--
2.56.0