> In your patch, you lost 'org-mode-line-clock-overrun face
> application. Please, fix it.

I have updated the patch to ensure that 'org-mode-line-clock-overrun is applied
properly when the effort time exceeds the estimated time.  I also added some
tests to catch the regression.  However, I made these changes *before* I saw 
that you'd attached a revised version of my patch with formatting changes.  I 
think I merged your formatting changes in with updates I made for the
'org-mode-line-clock-overrun face, but please take another look to make sure I
didn't drop anything in the merge process.

Thanks,
Rohit
From 17cd390ab54edc6713d09fff36ad0eddc9510b53 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              | 100 ++++++++++++++++++++++--------
 testing/lisp/test-org-clock.el | 108 ++++++++++++++++++++++++++++++---
 3 files changed, 184 insertions(+), 33 deletions(-)

diff --git a/etc/ORG-NEWS b/etc/ORG-NEWS
index 4e9440b51..62502a678 100644
--- a/etc/ORG-NEWS
+++ b/etc/ORG-NEWS
@@ -453,6 +453,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 a4b37ce59..f00ef9011 100644
--- a/lisp/org-clock.el
+++ b/lisp/org-clock.el
@@ -741,27 +741,80 @@ 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))))
+01:30/01:50 format (clocked/estimated).  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))
+         (clock-format-str (propertize "[%s]" 'face 'org-mode-line-clock))
+         (clock-format-effort-str (propertize "[%s/%s]"
+                                              'face
+                                              'org-mode-line-clock))
+         (mode-line-str-with-headline (propertize "%s (%s) "
+                                                  'face
+                                                  'org-mode-line-clock))
+         (mode-line-str-without-headline (propertize "%s "
+                                                     'face
+                                                     'org-mode-line-clock))
+         (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 clock-format-str clock-str)
+                     (format clock-format-effort-str
+                             (propertize clock-str
+                                         'face
+                                         (if (and org-clock-task-overrun
+                                                  (not
+                                                   org-clock-task-overrun-text))
+                                             'org-mode-line-clock-overrun
+                                           'org-mode-line-clock))
+                             effort-estimate-str)))
+         (spaces-and-parens-length (1+ (length
+                                        (format
+                                         mode-line-str-with-headline "" ""))))
+         (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 mode-line-str-with-headline time-str org-clock-heading))
+          ((or (<= max-string-length 0)
+               (<= max-string-length (+ spaces-and-parens-length
+                                        (length time-str))))
+           (format mode-line-str-without-headline
+                   (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 mode-line-str-with-headline
+                     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 +834,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..99c6cf68e 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,101 @@ 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)))))
+
+  ;; Verify that 'org-mode-line-clock-overrun face is applied if the task has
+  ;; overrun is alloted time and there is no overrun text defined
+  (should
+   (equal 'org-mode-line-clock-overrun
+          (org-test-with-temp-text
+              "* Heading
+:PROPERTIES:
+:EFFORT: 1h
+:END:"
+            (org-clock-in)
+            (prog1
+                (let ((org-clock-task-overrun t)
+                      (org-clock-task-overrun-text nil))
+                  (get-text-property 1 'face (org-clock-get-clock-string)))
+              (org-clock-out)))))
+
+  ;; Verify that the 'org-mode-line-clock face is applied if the task has not
+  ;; overrun its alloted time
+  (should
+   (equal 'org-mode-line-clock
+          (org-test-with-temp-text
+              "* Heading
+:PROPERTIES:
+:EFFORT: 1h
+:END:"
+            (org-clock-in)
+            (prog1
+                (get-text-property 1 'face (org-clock-get-clock-string))
+              (org-clock-out))))))
+
+
 
 ;;; Helpers
 
-- 
2.49.0

Reply via email to