I have updated the docstring for org-clock-get-clock-string, which I'd forgotten
to do in the previous version of the patch.

Thanks,
Rohit
From ba1662b116e8c0601c7f174d379005ac767f7f52 Mon Sep 17 00:00:00 2001
From: Rohit Patnaik <quanti...@gmail.com>
Date: Tue, 18 Mar 2025 04:45:06 -0500
Subject: [PATCH] org-clock: Make headline truncation behave better

* lisp/org-clock.el (org-clock-get-clock-string): Move the headline truncation
logic into `org-clock-get-clock-string', which enables much nicer truncation
behavior.  Now, `org-clock-get-clock-string' accepts an optional `max-length'
parameter.  If the length of the combined time string and headline exceeds
`max-length', the function truncates the headline, adds an ellipsis and
preserves the closing parenthesis.  If `max-length' is so small that even a
single character of the headline cannot be displayed, the function returns a
(possibly truncated) time string

* lisp/org-clock.el (org-clock-update-mode-line): Removed truncation code, as it
is now redundant with the truncation code in `org-clock-get-clock-string`.
Instead, the function now passes `org-clock-string-limit' to
`org-clock-get-clock-string' as the `max-length' argument.

* testing/lisp/test-org-clock.el (test-org-clock/mode-line): Added a few tests
to ensure that the new truncation logic is behaving correctly.

* etc/ORG-NEWS: (~org-clock-get-clock-string~ now takes an optional ~max-length~
argument): Document the change.
---
 etc/ORG-NEWS                   |  9 ++++
 lisp/org-clock.el              | 73 +++++++++++++++++++++-----------
 testing/lisp/test-org-clock.el | 76 ++++++++++++++++++++++++++++++----
 3 files changed, 126 insertions(+), 32 deletions(-)

diff --git a/etc/ORG-NEWS b/etc/ORG-NEWS
index 982bac4e9..3ab824199 100644
--- a/etc/ORG-NEWS
+++ b/etc/ORG-NEWS
@@ -426,6 +426,15 @@ When CHILDREN contains ~nil~ elements, they are skipped.  This way,
 
 will yield expected results rather than assigning literal ~nil~ as a child.
 
+*** ~org-clock-get-clock-string~ now takes an optional ~max-length~ argument
+
+When a ~max-length~ is passed to ~org-clock-get-clock-string~, it will first
+attempt to truncate the headline and add an ellipsis in order to make the entire
+clock string fit under the length limit. If the length limit is too small to
+accommodate even a single character of the headline, after accounting for spaces
+and the surrounding parentheses, it will omit the headline entirely and just
+show as much of the clock as fits under the limit.
+
 ** Removed or renamed functions and variables
 
 *** ~org-cycle-display-inline-images~ is renamed to ~org-cycle-display-link-previews~
diff --git a/lisp/org-clock.el b/lisp/org-clock.el
index d72ef4e29..2d0f223d7 100644
--- a/lisp/org-clock.el
+++ b/lisp/org-clock.el
@@ -741,27 +741,55 @@ pointing to it."
 (defvar org-clock-update-period 60
   "Number of seconds between mode line clock string updates.")
 
-(defun org-clock-get-clock-string ()
+(defun org-clock-get-clock-string (&optional max-length)
   "Form a clock-string, that will be shown in the mode line.
 If an effort estimate was defined for the current item, use
 01:30/01:50 format (clocked/estimated).
-If not, show simply the clocked time like 01:50."
-  (let ((clocked-time (org-clock-get-clocked-time)))
-    (if org-clock-effort
-	(let* ((effort-in-minutes (org-duration-to-minutes org-clock-effort))
-	       (work-done-str
-		(propertize (org-duration-from-minutes clocked-time)
-			    'face
-			    (if (and org-clock-task-overrun
-				     (not org-clock-task-overrun-text))
-				'org-mode-line-clock-overrun
-			      'org-mode-line-clock)))
-	       (effort-str (org-duration-from-minutes effort-in-minutes)))
-	  (format (propertize "[%s/%s] (%s) " 'face 'org-mode-line-clock)
-		  work-done-str effort-str org-clock-heading))
-      (format (propertize "[%s] (%s) " 'face 'org-mode-line-clock)
-	      (org-duration-from-minutes clocked-time)
-	      org-clock-heading))))
+If not, show simply the clocked time like 01:50.  When the optional max-length
+argument is given, this function will preferentially truncate the headline in 
+order to ensure that the entire clock string's length remains under the limit."
+  (let* ((max-string-length (or max-length 0))
+         (clocked-time (org-clock-get-clocked-time))
+         (clock-str (org-duration-from-minutes clocked-time))
+         (effort-estimate-str (if org-clock-effort
+                         (org-duration-from-minutes
+                          (org-duration-to-minutes
+                           org-clock-effort))
+                       nil))
+         (time-str (if (not org-clock-effort)
+                       (format "[%s]" clock-str)
+                     (format "[%s/%s]" clock-str effort-estimate-str)))
+         (spaces-and-parens-length 5)
+         (untruncated-length (+ spaces-and-parens-length (length time-str)
+                                (length org-clock-heading))))
+    ;; There are three cases for displaying the mode-line clock string.
+    ;; 1. MAX-STRING-LENGTH is zero or greater than UNTRUNCATED-LENGTH
+    ;;      - We can display the clock and the headline without truncation
+    ;; 2. MAX-STRING-LENGTH is above zero and less than or equal to
+    ;;    (+ SPACES-AND-PARENS-LENGTH (LENGTH TIME-STR))
+    ;;      - There isn't enough room to display any of the headline so just
+    ;;        display a (truncated) time string
+    ;; 3. ORG-CLOCK-STRING-LIMIT is greater than
+    ;;    (+ SPACES-AND-PARENS-LENGTH (LENGTH TIME-STR)) but less than
+    ;;    UNTRUNCATED-LENGTH
+    ;;      - Intelligently truncate the headline such that the total length of
+    ;;        the mode line string is less than ORG-CLOCK-STRING-LIMIT
+    (cond ((or (<= max-string-length 0)
+               (>= max-string-length untruncated-length))
+           (format (propertize "%s (%s) " 'face 'org-mode-line-clock)
+                   time-str org-clock-heading))
+          ((or (<= max-string-length 0)
+               (<= max-string-length (+ spaces-and-parens-length (length time-str))))
+           (format (propertize "%s " 'face 'org-mode-line-clock)
+                   (substring time-str 0 (min (length time-str)
+                                              max-string-length))))
+          (t
+           (let ((heading-length (- max-string-length
+                                    (+ spaces-and-parens-length (length time-str)))))
+             (format (propertize "%s (%s) " 'face 'org-mode-line-clock)
+                     time-str (string-join `(,(substring org-clock-heading
+                                                         0 heading-length)
+                                             "…"))))))))
 
 (defun org-clock-get-last-clock-out-time ()
   "Get the last clock-out time for the current subtree."
@@ -781,15 +809,10 @@ When optional argument is non-nil, refresh cached heading."
   (when refresh (setq org-clock-heading (org-clock--mode-line-heading)))
   (setq org-mode-line-string
 	(propertize
-	 (let ((clock-string (org-clock-get-clock-string))
+	 (let ((clock-string (org-clock-get-clock-string org-clock-string-limit))
 	       (help-text "Org mode clock is running.\nmouse-1 shows a \
 menu\nmouse-2 will jump to task"))
-	   (if (and (> org-clock-string-limit 0)
-		    (> (length clock-string) org-clock-string-limit))
-	       (propertize
-		(substring clock-string 0 org-clock-string-limit)
-		'help-echo (concat help-text ": " org-clock-heading))
-	     (propertize clock-string 'help-echo help-text)))
+	   (propertize clock-string 'help-echo help-text))
 	 'local-map org-clock-mode-line-map
 	 'mouse-face 'mode-line-highlight))
   (if (and org-clock-task-overrun org-clock-task-overrun-text)
diff --git a/testing/lisp/test-org-clock.el b/testing/lisp/test-org-clock.el
index 17f71d492..039dc5071 100644
--- a/testing/lisp/test-org-clock.el
+++ b/testing/lisp/test-org-clock.el
@@ -1298,12 +1298,12 @@ Variables'."
    (equal
     "<before> [0:00] (Heading) <after> "
     (org-test-with-temp-text
-        "* Heading"
-      (org-clock-in)
-      (prog1 (concat "<before> "
-                     (org-clock-get-clock-string)
-                     "<after> ")
-        (org-clock-out)))))
+     "* Heading"
+     (org-clock-in)
+     (prog1 (concat "<before> "
+                    (org-clock-get-clock-string)
+                    "<after> ")
+       (org-clock-out)))))
   ;; Test the variant with effort.
   (should
    (equal
@@ -1317,7 +1317,69 @@ Variables'."
       (prog1 (concat "<before> "
                      (org-clock-get-clock-string)
                      "<after> ")
-        (org-clock-out))))))
+        (org-clock-out)))))
+  ;; Verify that long headlines are truncated correctly
+  (should
+   (equal
+    "<before> [0:00] (This is a…) <after> "
+    (org-test-with-temp-text
+     "* This is a long headline blah blah blah"
+     (org-clock-in)
+     (prog1 (concat "<before> "
+                    (org-clock-get-clock-string 20)
+                    "<after> ")
+       (org-clock-out)))))
+  ;; Verify that long headlines with effort are truncated correctly
+  (should
+   (equal
+    "<before> [0:00/1:00] (This…) <after> "
+    (org-test-with-temp-text
+     "* This is a long headline blah blah blah
+:PROPERTIES:
+:EFFORT: 1h
+:END:"
+     (org-clock-in)
+     (prog1 (concat "<before> "
+                    (org-clock-get-clock-string 20)
+                    "<after> ")
+       (org-clock-out)))))
+
+  ;; Check the limit case where there's just one character of the headline
+  ;; displayed
+  (should
+   (equal
+    "<before> [0:00] (T…) <after> "
+    (org-test-with-temp-text
+     "* This is a long headline blah blah blah"
+     (org-clock-in)
+     (prog1 (concat "<before> "
+                    (org-clock-get-clock-string 12)
+                    "<after> ")
+       (org-clock-out)))))
+
+  ;; Check the limit case where the headline can't be displayed at all
+  (should
+   (equal
+    "<before> [0:00] <after> "
+    (org-test-with-temp-text
+     "* This is a long headline blah blah blah"
+     (org-clock-in)
+     (prog1 (concat "<before> "
+                    (org-clock-get-clock-string 10)
+                    "<after> ")
+       (org-clock-out)))))
+
+  ;; Check the limit case where even the time string is truncated
+  (should
+   (equal
+    "<before> [0: <after> "
+    (org-test-with-temp-text
+     "* This is a long headline blah blah blah"
+     (org-clock-in)
+     (prog1 (concat "<before> "
+                    (org-clock-get-clock-string 3)
+                    "<after> ")
+       (org-clock-out))))))
 
 ;;; Helpers
 
-- 
2.49.0

Reply via email to