Hello,
Currently =org-clock-string-limit= truncates rather crudely. It just does a
=substring=, which results in the closing parenthesis of the clock string
getting cut off. The attached patch seeks to improve this truncation behavior
by trimming the headline such that the closing paren can be displayed. It also
adds an ellipsis (…) to make it more obvious that the headline has been
truncated.
I haven't updated the news file yet, because I wanted to get feedback from the
list on my approach first. Once you're all happy with the code, I'll finalize it
by adding the necessary news and documentation updates.
Thanks,
Rohit Patnaik
From f8c1f2919d8aa07e6f73ad5b097f8da2bb1ca3ee 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, when the length of the time string and the headline exceeds
`org-clock-string-limit`, org-clock truncates the headline, adds an ellipsis,
and preserves the closing parenthesis. If `org-clock-string-limit` does not
permit displaying a single character of the headline, we just display 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`.
* testing/lisp/test-org-clock.el (test-org-clock/mode-line): Added a few tests
to ensure that the new truncation logic was behaving correctly.
---
lisp/org-clock.el | 62 +++++++++++++++++---------
testing/lisp/test-org-clock.el | 81 +++++++++++++++++++++++++++++++---
2 files changed, 114 insertions(+), 29 deletions(-)
diff --git a/lisp/org-clock.el b/lisp/org-clock.el
index d72ef4e29..9c89fb44a 100644
--- a/lisp/org-clock.el
+++ b/lisp/org-clock.el
@@ -746,22 +746,45 @@ pointing to it."
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))))
+ (let* ((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)))
+ (untruncated-length (+ 5 (length time-str)
+ (length org-clock-heading))))
+ ;; There are three cases for displaying the mode-line clock string.
+ ;; 1. ORG-CLOCK-STRING-LIMIT is zero or greater than UNTRUNCATED-LENGTH
+ ;; - We can display the clock and the headline without truncation
+ ;; 2. ORG-CLOCK-STRING-LIMIT is above zero and less than or equal to
+ ;; (+ 5 (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 (+ 5 (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 (<= org-clock-string-limit 0)
+ (>= org-clock-string-limit untruncated-length))
+ (format (propertize "%s (%s) " 'face 'org-mode-line-clock)
+ time-str org-clock-heading))
+ ((or (<= org-clock-string-limit 0)
+ (<= org-clock-string-limit (+ 5 (length time-str))))
+ (format (propertize "%s " 'face 'org-mode-line-clock)
+ (substring time-str 0 (min (length time-str)
+ org-clock-string-limit))))
+ (t
+ (let ((heading-length (- org-clock-string-limit
+ (+ 5 (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."
@@ -784,12 +807,7 @@ When optional argument is non-nil, refresh cached heading."
(let ((clock-string (org-clock-get-clock-string))
(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..3757ecd25 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,74 @@ 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> "
+ (let ((org-clock-string-limit 20))
+ (org-test-with-temp-text
+ "* This is a long headline blah blah blah"
+ (org-clock-in)
+ (prog1 (concat "<before> "
+ (org-clock-get-clock-string)
+ "<after> ")
+ (org-clock-out))))))
+ ;; Verify that long headlines with effort are truncated correctly
+ (should
+ (equal
+ "<before> [0:00/1:00] (Thisâ¦) <after> "
+ (let ((org-clock-string-limit 20))
+ (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)
+ "<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> "
+ (let ((org-clock-string-limit 12))
+ (org-test-with-temp-text
+ "* This is a long headline blah blah blah"
+ (org-clock-in)
+ (prog1 (concat "<before> "
+ (org-clock-get-clock-string)
+ "<after> ")
+ (org-clock-out))))))
+
+ ;; Check the limit case where the headline can't be displayed at all
+ (should
+ (equal
+ "<before> [0:00] <after> "
+ (let ((org-clock-string-limit 10))
+ (org-test-with-temp-text
+ "* This is a long headline blah blah blah"
+ (org-clock-in)
+ (prog1 (concat "<before> "
+ (org-clock-get-clock-string)
+ "<after> ")
+ (org-clock-out))))))
+
+ ;; Check the limit case where even the time string is truncated
+ (should
+ (equal
+ "<before> [0: <after> "
+ (let ((org-clock-string-limit 3))
+ (org-test-with-temp-text
+ "* This is a long headline blah blah blah"
+ (org-clock-in)
+ (prog1 (concat "<before> "
+ (org-clock-get-clock-string)
+ "<after> ")
+ (org-clock-out)))))))
;;; Helpers
--
2.48.1