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