branch: externals/org commit 7eafc194d2c3a42b84a319847f66ec6bdfea183c Author: Rohit Patnaik <quanti...@gmail.com> Commit: Ihor Radchenko <yanta...@posteo.net>
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 | 96 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 178 insertions(+), 27 deletions(-) diff --git a/etc/ORG-NEWS b/etc/ORG-NEWS index 4e9440b519..62502a6781 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 a4b37ce59e..efac5212a2 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 17f71d4927..8a196ee963 100644 --- a/testing/lisp/test-org-clock.el +++ b/testing/lisp/test-org-clock.el @@ -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