Björn Kettunen <[email protected]> writes:

> Ihor Radchenko <[email protected]> writes:
>
>> Björn Kettunen <[email protected]> writes:
>>
>>>> Any particular reason why you don't use the code I suggested?
>>>
>>> Mostly because I didn't see your reply until I posted the updated patch.
>>>
>>> But after reading the suggested code now it's also that it didn't seem
>>> obvious to me that it can be shortened that much essentially you do the
>>> same thin as I'm already doing but without the check if a table contains
>>> any data.
>>
>> Is that check necessary though? org-clock-get-table-data always returns
>> non-empty list.
>
> The list doesn't have to be empty but it can return a list that contains
> a list without any entries, the when-let takes care of that.
>
>>> Also I think it's easier to read wrap the table merging and the
>>> expanding of files with their archives into separate functions.
>>> The only thing that bothers me that I don't know how to call the
>>> get-table-data-with-archives function without the use of a lambda when
>>> passing both the parameters and the file.
>>
>> I am not sure why using lambda is a problem.
>
> It's not really but it's just a bit annoying to wrap the call into a
> lamda just to be able to pass more arguments to a function. Anyway it's
> fine.
>
>>> What's the difference between using nthcdr 2 and cddr?
>>
>> The latter is slightly faster. That's it.
>>
>>> Instead of car and then nthcdr or cddr I could probably also do cddar.
>>
>> Sure, you can.

The updated patch below:
>From dfd9aae1d39b64ba942fbe66fc4b7c2366b50a13 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bj=C3=B6rn=20Kettunen?= <[email protected]>
Date: Tue, 3 Mar 2026 22:47:12 +0200
Subject: [PATCH 1/2] org-clock: clock-report refactor list and function scope

* lisp/org-clock.el (org-dblock-write:clocktable): Expand
files in directories if any of the entries in scope is a directory.
Just like in org-agenda-files.  Function scope is now evaluated
before any other scope and can return a scope by itself.

(org-clock-merge-table-data): A new function which can
merge clocktable data for a list of table belonging to a file.

(org-clock-get-table-data-with-archives): A new function to get table
data for a file with it's archives.

* lisp/org.el (org-file-list-expand):
(org-agenda-files): Refactor file expansion into separate function.

* doc/org-manual.org: Document.
* etc/ORG-NEWS: Announce
---
 doc/org-manual.org |  20 ++++-----
 etc/ORG-NEWS       |  23 ++++++++++
 lisp/org-clock.el  | 104 +++++++++++++++++++++++++++++++--------------
 lisp/org.el        |  20 ++++++---
 4 files changed, 119 insertions(+), 48 deletions(-)

diff --git a/doc/org-manual.org b/doc/org-manual.org
index 904e1270d..d282f419a 100644
--- a/doc/org-manual.org
+++ b/doc/org-manual.org
@@ -7103,16 +7103,16 @@ *** The clock table
 
   The scope to consider.  This can be any of the following:
 
-  | =nil=                  | the current buffer or narrowed region                               |
-  | =file=                 | the full current buffer                                             |
-  | =subtree=              | the subtree where the clocktable is located                         |
-  | =treeN=                | the surrounding level N tree, for example =tree3=                   |
-  | =tree=                 | the surrounding level 1 tree                                        |
-  | =agenda=               | all agenda files                                                    |
-  | =("file" ...)=         | scan these files                                                    |
-  | =FUNCTION=             | scan files returned by calling {{{var(FUNCTION)}}} with no argument |
-  | =file-with-archives=   | current file and its archives                                       |
-  | =agenda-with-archives= | all agenda files, including archives                                |
+  | =nil=                         | the current buffer or narrowed region                                             |
+  | =file=                        | the full current buffer                                                           |
+  | =subtree=                     | the subtree where the clocktable is located                                       |
+  | =treeN=                       | the surrounding level N tree, for example =tree3=                                 |
+  | =tree=                        | the surrounding level 1 tree                                                      |
+  | =agenda=                      | all agenda files                                                                  |
+  | =file-with-archives=          | current file and its archives                                                     |
+  | =agenda-with-archives=        | all agenda files, including archives                                              |
+  | =([scope] "file" "dir" "...)= | scan these files or files in directories, scope can be file or file-with-archives |
+  | f=FUNCTION=                   | call {{{var(FUNCTION)}}} with no argument process any scope it returns            |
 
 - =:block= ::
 
diff --git a/etc/ORG-NEWS b/etc/ORG-NEWS
index 2dcd86aee..a20dc685e 100644
--- a/etc/ORG-NEWS
+++ b/etc/ORG-NEWS
@@ -86,6 +86,20 @@ This tangles the block to four files:
 - /backup/config.el
 - /backup/backup.el
 
+*** Clocktable options =:scope function= and =:scope (file)= have changed.
+
+- ~function~
+  Now called before any scope is processed.
+  It can now also return any other scope by itself which is then
+  processed after.
+
+- ~(file)~
+  Can now also include directories which are resolved just like in
+   ~org-agenda-files~.
+  The first entry of this list can also be either ~file~ or ~file-archives~.
+  If this is the case then all following entries are considered
+  like the ~file~ or ~file-with-archives~ scope.
+
 ** New and changed options
 
 # Changes dealing with changing default values of customizations,
@@ -106,6 +120,15 @@ variable.
 Given the completed and total number of tasks, format the percent
 cookie =[N%]=.
 
+*** New function ~org-agenda-directory-files-recursively~
+
+Expand list of flies according to ~org-agenda-file-regexp~.
+
+*** New function ~org-clock-merge-table-data~
+
+Takes a list of clocktable data tables, merge them all
+under file or the file name of the first table.
+
 ** Removed or renamed functions and variables
 
 ** Miscellaneous
diff --git a/lisp/org-clock.el b/lisp/org-clock.el
index ce2d23a9b..99f1f0684 100644
--- a/lisp/org-clock.el
+++ b/lisp/org-clock.el
@@ -2677,22 +2677,21 @@ (defun org-dblock-write:clocktable (params)
   (catch 'exit
     (let* ((scope (plist-get params :scope))
 	   (base-buffer (org-base-buffer (current-buffer)))
+           (scope (or (and (functionp scope)
+                           (funcall scope))
+                      scope))
 	   (files (pcase scope
-		    (`agenda
+		    ((or `agenda `agenda-with-archives)
 		     (org-agenda-files t))
-		    (`agenda-with-archives
-		     (org-add-archive-files (org-agenda-files t)))
-		    (`file-with-archives
-		     (let ((base-file (buffer-file-name base-buffer)))
-		       (and base-file
-			    (org-add-archive-files (list base-file)))))
-		    ((or `nil `file `subtree `tree
+                    ((or `file-with-archives)
+                     (list (buffer-file-name base-buffer)))
+		    ((or `nil `subtree `tree `file
 			 (and (pred symbolp)
 			      (guard (string-match "\\`tree\\([0-9]+\\)\\'"
 						   (symbol-name scope)))))
 		     base-buffer)
-		    ((pred functionp) (funcall scope))
 		    ((pred consp) scope)
+                    ((pred stringp) scope) ;; To not break previous function calls here
 		    (_ (user-error "Unknown scope: %S" scope))))
 	   (block (plist-get params :block))
 	   (ts (plist-get params :tstart))
@@ -2704,7 +2703,23 @@ (defun org-dblock-write:clocktable (params)
 	   (formatter (or (plist-get params :formatter)
 			  org-clock-clocktable-formatter
 			  'org-clocktable-write-default))
-	   cc)
+           (multifile
+	    ;; Even though `file-with-archives' can consist of
+	    ;; multiple files, we consider this is one extended file
+	    ;; instead.
+	    (and (not hide-files)
+		 (consp files)
+		 (not (eq scope 'file-with-archives))))
+           cc)
+
+      (when (consp files)
+        (when-let* ((cons-scope (car files))
+                    (cons-scope (and (symbolp cons-scope)
+                                     cons-scope)))
+          (setq scope cons-scope)
+          (setq files (cdr files)))
+        (setq files (org-agenda-directory-files-recursively files)))
+
       ;; Check if we need to do steps
       (when block
 	;; Get the range text for the header
@@ -2718,20 +2733,22 @@ (defun org-dblock-write:clocktable (params)
 	(org-clocktable-steps params)
 	(throw 'exit nil))
 
-      (org-agenda-prepare-buffers (if (consp files) files (list files)))
+      (unless (consp files)
+        (org-agenda-prepare-buffers (list files)))
 
       (let ((origin (point))
 	    (tables
-	     (if (consp files)
-		 (mapcar (lambda (file)
-			   (with-current-buffer (find-buffer-visiting file)
-			     (save-excursion
-			       (save-restriction
-				 (org-clock-get-table-data file params)))))
-			 files)
+             (if (consp files)
+                 (if (eq scope 'file-with-archives)
+                     (mapcar (lambda (file)
+                               (org-clock-get-table-data-with-archives
+                                file params)) files)
+                   (mapcar  (lambda (file)
+                              (org-clock--get-table-data1 file params))
+		            files))
 	       ;; Get the right restriction for the scope.
 	       (save-restriction
-		 (cond
+	         (cond
 		  ((not scope))	     ;use the restriction as it is now
 		  ((eq scope 'file) (widen))
 		  ((eq scope 'subtree) (org-narrow-to-subtree))
@@ -2739,25 +2756,18 @@ (defun org-dblock-write:clocktable (params)
 		   (while (org-up-heading-safe))
 		   (org-narrow-to-subtree))
 		  ((and (symbolp scope)
-			(string-match "\\`tree\\([0-9]+\\)\\'"
+		        (string-match "\\`tree\\([0-9]+\\)\\'"
 				      (symbol-name scope)))
 		   (let ((level (string-to-number
-				 (match-string 1 (symbol-name scope)))))
+			         (match-string 1 (symbol-name scope)))))
 		     (catch 'exit
 		       (while (org-up-heading-safe)
-			 (looking-at org-outline-regexp)
-			 (when (<= (org-reduced-level (funcall outline-level))
+		         (looking-at org-outline-regexp)
+		         (when (<= (org-reduced-level (funcall outline-level))
 				   level)
 			   (throw 'exit nil))))
 		     (org-narrow-to-subtree))))
-		 (list (org-clock-get-table-data nil params)))))
-	    (multifile
-	     ;; Even though `file-with-archives' can consist of
-	     ;; multiple files, we consider this is one extended file
-	     ;; instead.
-	     (and (not hide-files)
-		  (consp files)
-		  (not (eq scope 'file-with-archives)))))
+	         (list (org-clock-get-table-data nil params))))))
 
 	(funcall formatter
 		 origin
@@ -3112,6 +3122,38 @@ (defun org-clocktable-steps (params)
         (setq start next))
       (end-of-line 0))))
 
+(defun org-clock--get-table-data1 (file params)
+  "Get clocktable-data for FILE with PARAMS."
+  (org-agenda-prepare-buffers (list file))
+  (with-current-buffer
+    (find-buffer-visiting file)
+    (save-excursion
+      (save-restriction
+        (org-clock-get-table-data file params)))))
+
+(defun org-clock-get-table-data-with-archives (file params)
+  "Get clocktable data with archives for FILE with parameters PARAMS.
+If file-only don't add-archives"
+  (let* ((file-plus-archives (org-add-archive-files (list file)))
+         (tables (mapcar (lambda (file)
+                           (org-clock--get-table-data1 file params))
+                         file-plus-archives)))
+    (org-clock-merge-table-data tables file)))
+
+(defun org-clock-merge-table-data (tables &optional file)
+  "Merge table data for TABLES for FILE.
+When FILE isn't given assume FILE as the file of the first table.
+TABLES is list of table data returned in the format returned by
+`org-clock-get-table-data'.
+Returns the same tables but with each table merged."
+  (let* ((file (or file (caaar tables)))
+         (total-time 0) entries)
+    (while-let ((table (pop tables)))
+      (incf total-time (nth 1 table))
+      (when-let* ((new-entries (cddar table)))
+        (setq entries (append new-entries entries))))
+  (list file total-time entries)))
+
 (defun org-clock-get-table-data (file params)
   "Get the clocktable data for file FILE, with parameters PARAMS.
 FILE is only for identification - this function assumes that
diff --git a/lisp/org.el b/lisp/org.el
index fc51d4ba3..05607bd2c 100644
--- a/lisp/org.el
+++ b/lisp/org.el
@@ -15977,6 +15977,18 @@ (defun org-switchb (&optional arg)
 		      (mapcar #'list (mapcar #'buffer-name blist))
 		      nil t))))
 
+
+(defun org-agenda-directory-files-recursively (files)
+  "Expand list of FILES according to `org-agenda-file-regexp'."
+  (apply 'append
+	 (mapcar (lambda (f)
+		   (if (file-directory-p f)
+		       (directory-files
+			f t org-agenda-file-regexp)
+		     (list (expand-file-name f org-directory))))
+		 files)))
+
+
 (defun org-agenda-files (&optional unrestricted archives)
   "Get the list of agenda files.
 Optional UNRESTRICTED means return the full list even if a restriction
@@ -15990,13 +16002,7 @@ (defun org-agenda-files (&optional unrestricted archives)
 	  ((stringp org-agenda-files) (org-read-agenda-file-list))
 	  ((listp org-agenda-files) org-agenda-files)
 	  (t (error "Invalid value of `org-agenda-files'")))))
-    (setq files (apply 'append
-		       (mapcar (lambda (f)
-				 (if (file-directory-p f)
-				     (directory-files
-				      f t org-agenda-file-regexp)
-				   (list (expand-file-name f org-directory))))
-			       files)))
+    (setq files (org-agenda-directory-files-recursively files))
     (when org-agenda-skip-unavailable-files
       (setq files (delq nil
 			(mapcar (lambda (file)
-- 
2.53.0

Reply via email to