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

> Björn Kettunen <[email protected]> writes:
>
>> Ihor Radchenko <[email protected]> writes:
>>
>>> Björn Kettunen <[email protected]> writes:
>>>
>>>>>> Ideally I would like that similar to single file-with-archices that
>>>>>> archive and the their files are shown as one file.
>>>>>> But not sure how to do that.
>>>>>
>>>>> That's possible to do by combining the results coming from file and its
>>>>> archives together.
>>>>
>>>> How would you do this?
>>>
>>> (mapcar (lambda (file)
>>>                        (with-current-buffer (find-buffer-visiting file)
>>>                          (save-excursion
>>>                            (save-restriction
>>>                              (org-clock-get-table-data file params)))))
>>>                      files)
>>>
>>> In the above, you will need to merge table data coming from files and their 
>>> archives.
>>
>> Looks good. I think I sort the tables and then  merge the entries next
>> to each other if there name without extension matches.
>> However the thing I haven't figure out yet is to how to merge the table
>> data here.
>>
>> Any clues?
>
>
> I think I got something. I wrote a function that merges table with
> matching file-names as keys based of the description in
> org-clock-get-table-data.
>
> The result is very similar to what file-with-archives for a single file
> did before the patch.
> I.e. headings from main file and archive are not merged.
>
>
> I haven't updated the documentation or removed any of the commented out
> code.
>
> I'm mostly looking for feedback on how the merge table data function is
> called
> and on the function itself.

Here's an updated patch with the documentation and changelog changes
included.

>From 9f2711c8404cc93ca951b04f0bd58edd1b2a5a07 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 tables where
the file name without extension is the same.

* 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  | 120 ++++++++++++++++++++++++++++++++++++---------
 lisp/org.el        |  20 +++++---
 4 files changed, 142 insertions(+), 41 deletions(-)

diff --git a/doc/org-manual.org b/doc/org-manual.org
index 9c4c27877..0f03e62a4 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 40fa1e6aa..95e3edc0f 100644
--- a/etc/ORG-NEWS
+++ b/etc/ORG-NEWS
@@ -40,6 +40,20 @@ into a "Tags" section and a "Priorities" section.
 Priorities can now be increased, decreased, set to the default, and
 set interactively from the priority context menus.
 
+*** 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,
@@ -60,6 +74,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. Merges them according
+to their file name without suffix.
+
 ** Removed or renamed functions and variables
 
 ** Miscellaneous
diff --git a/lisp/org-clock.el b/lisp/org-clock.el
index ce2d23a9b..637103369 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,25 @@ (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)))
+      (pcase scope ((or `agenda-with-archives `file-with-archives)
+                    (setq files (org-add-archive-files files))))
+
       ;; Check if we need to do steps
       (when block
 	;; Get the range text for the header
@@ -2723,12 +2740,38 @@ (defun org-dblock-write:clocktable (params)
       (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)
+                 ;;      (let* ((files (sort files :lessp #'string-lessp))
+                 ;;             (previous-file (nth 0 files))
+                 ;;             previous-table
+                 ;;             previous-file
+                 ;;             (tables
+                 ;; table)
+                 ;;          (while-let ((file (pop files)))
+                 ;;            (setq table (with-current-buffer (find-buffer-visiting file)
+		 ;;                          (save-excursion
+		 ;;                            (save-restriction
+                 ;;                              (org-clock-get-table-data file params)))))
+                 ;;            (if (equal (file-name-sans-extension file)
+                 ;;                       previous-file)
+                 ;;                (progn
+                 ;;                  (setf tables (push (cl-union table previous-table) tables))
+                 ;;                  (setf previous-file nil))
+                 ;;              (setf previous-file (file-name-sans-extension file))
+                 ;;              (setf tables (push previous-table tables))
+                 ;;              (setf previous-table table))))
+                 (let ((tables
+		        (mapcar
+                         (lambda (file)
+		           (with-current-buffer
+                               (find-buffer-visiting file)
+		             (save-excursion
+		               (save-restriction
+                                 (org-clock-get-table-data file params)))))
+		         files)))
+                   (if (eq scope 'file-with-archives)
+                       (setq tables (org-clock-merge-table-data tables))
+                     tables))
+
 	       ;; Get the right restriction for the scope.
 	       (save-restriction
 		 (cond
@@ -2750,14 +2793,7 @@ (defun org-dblock-write:clocktable (params)
 				   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 +3148,42 @@ (defun org-clocktable-steps (params)
         (setq start next))
       (end-of-line 0))))
 
+
+(defun org-clock-merge-table-data (tables)
+  "Merge table data for TABLES.
+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
+where the file name without extension is the same."
+  (let ((table-keys (mapcar #'file-name-sans-extension
+                            (mapcar #'car tables)))
+        (index-tables (copy-tree tables))
+        (index 0))
+    (while-let ((table (pop index-tables))
+                (key (nth index table-keys)))
+      (setf (nth index table-keys) nil)
+      (when-let* ((elm-pos (seq-position table-keys key))
+                  (table-a (nth elm-pos tables))
+                  (table-b (nth index tables))
+                  (total-time (+ (nth 1 table-a)
+                                 (nth 1 table-b)))
+                  (file-name (concat
+                              key
+                              ".org")))
+
+        (when-let* ((table-entries-a (car (nthcdr 2 table-b)))
+                    (table-entries-b (car (nthcdr 2 table-a)))
+                    (table-entries  (list (append table-entries-b
+                                                  table-entries-a))))
+          (setf (nth index tables)
+                (cl-merge 'list (list file-name total-time)
+                          table-entries #'equal)))
+
+        (setf (nth elm-pos table-keys) nil)
+        (setq tables (seq-remove-at-position tables elm-pos)))
+      (incf index))
+    tables))
+
 (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