branch: externals/tmr
commit 5df763d091559f08918b7a9276d1b4c97ad9bbc6
Author: Steven Allen <ste...@stebalien.com>
Commit: Steven Allen <ste...@stebalien.com>

    Add support for displaying timers in the mode-line
    
    This patch adds support for displaying active timers in the mode-line
    (specifically, the global-mode-string).
    
    - The number of timers displayed is configurable (defaults to 3).
    - The max description length is also configurable.
    - The timer will blink and change faces when it gets closer to expiring.
    - Timers only display seconds when within 2 minutes of expiring to avoid
      being too distracting.
    
    This package is best used when the global mode string is displayed in
    the tab-bar (by enabling tab-bar-mode and adding tab-bar-format-global
    to tab-bar-format).
---
 tmr-mode-line.el | 179 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 179 insertions(+)

diff --git a/tmr-mode-line.el b/tmr-mode-line.el
new file mode 100644
index 0000000000..c55c553a5e
--- /dev/null
+++ b/tmr-mode-line.el
@@ -0,0 +1,179 @@
+;;; tmr-mode-line.el --- Mode-line integration for tmr -*- lexical-binding: t 
-*-
+
+;; Copyright (C) 2025  Free Software Foundation, Inc.
+
+;;; Commentary:
+
+;; This package provides a mode-line component that displays active TMR May 
Ring
+;; timers with a countdown to when they fire.
+;;
+;; To use it, add the following to your init file:
+;;
+;; (require 'tmr-mode-line)
+;; (tmr-mode-line-mode 1)
+;;
+;; Customize the appearance with:
+;; - `tmr-mode-line-format': Format string for displaying each timer
+;; - `tmr-mode-line-separator': String used to separate multiple timers
+;; - `tmr-mode-line-max-timers': Maximum number of timers to display
+;; - `tmr-mode-line-max-desc-length': Max length for timer descriptions
+
+;;; Code:
+
+(require 'tmr)
+(require 'format-spec)
+(eval-when-compile (require 'subr-x))
+
+(defgroup tmr-mode-line nil
+  "Mode-line integration for TMR May Ring."
+  :link '(info-link :tag "Info Manual" "(tmr)")
+  :link '(url-link :tag "Homepage" "https://protesilaos.com/emacs/tmr";)
+  :link '(emacs-library-link :tag "Library Source" "tmr.el")
+  :group 'tmr)
+
+(defcustom tmr-mode-line-format "%r%d"
+  "Format string for displaying a timer in the mode-line.
+Available format specifiers:
+- %r: Remaining time.
+- %d: Timer description (truncated to `tmr-mode-line-max-desc-length')."
+  :type 'string
+  :group 'tmr-mode-line)
+
+(defcustom tmr-mode-line-separator " | "
+  "String used to separate multiple timers in the mode-line."
+  :type 'string
+  :group 'tmr-mode-line)
+
+(defcustom tmr-mode-line-max-timers 3
+  "Maximum number of timers to display in the mode-line.
+Set to nil to show all timers."
+  :type '(choice (const :tag "Show all" nil)
+                 (integer :tag "Maximum number"))
+  :group 'tmr-mode-line)
+
+(defcustom tmr-mode-line-max-desc-length 15
+  "Maximum length for timer descriptions in the mode-line.
+Longer descriptions will be truncated."
+  :type '(choice (const :tag "Don't truncate" nil)
+                 (integer :tag "Truncate"))
+  :group 'tmr-mode-line)
+
+(defcustom tmr-mode-line-prefix "⏰"
+  "Prefix string displayed before the timer list."
+  :type 'string
+  :group 'tmr-mode-line)
+
+(defface tmr-mode-line-active
+  '((t :inherit mode-line-emphasis))
+  "Face for active timers in the mode-line."
+  :group 'tmr-mode-line)
+
+(defface tmr-mode-line-soon
+  '((t :inherit warning))
+  "Face for timers that will expire in the next 2 minutes."
+  :group 'tmr-mode-line)
+
+(defface tmr-mode-line-urgent
+  '((t :inherit error))
+  "Face for timers that will expire in the next 30 seconds."
+  :group 'tmr-mode-line)
+
+(defvar tmr-mode-line-string nil
+  "TMR mode-line string.")
+(put 'tmr-mode-line-string 'risky-local-variable t)
+
+(defvar tmr-mode-line--update-timer nil
+  "Timer to update the mode-line.")
+
+(defun tmr-mode-line--format-remaining (timer)
+  "Format remaining time for TIMER with appropriate face."
+  (let* ((secs (float-time (time-subtract (tmr--timer-end-date timer) nil)))
+         (face (cond ((and (< secs 5) (evenp (truncate secs)))
+                      '((t :inherit tmr-mode-line-urgent :inverse-video t)))
+                     ((< secs 30) 'tmr-mode-line-urgent)
+                     ((= (truncate secs) 30)
+                      '((t :inherit tmr-mode-line-urgent :inverse-video t)))
+                     ((= (truncate secs) 60)
+                      '((t :inherit tmr-mode-line-soon :inverse-video t)))
+                     ((< secs 120) 'tmr-mode-line-soon)
+                     ((= (truncate secs) 120)
+                      '((t :inherit tmr-mode-line-soon :inverse-video t)))
+                     (t 'tmr-mode-line-active)))
+         (formatted (format-seconds
+                     (cond ((< secs 120) "%mm %ss%z")
+                           ((< secs (* 24 60 60)) "%hh %mm%z")
+                           (t "%dd %hh%z"))
+                     secs)))
+    (propertize formatted 'face face)))
+
+(defun tmr-mode-line--format-description (timer)
+  "Format description for TIMER, truncating if necessary."
+  (if-let* ((desc (tmr--timer-description timer)))
+    (concat " " (if tmr-mode-line-max-desc-length
+                    (truncate-string-to-width
+                     desc tmr-mode-line-max-desc-length
+                     nil nil t)
+                  desc))
+    ""))
+
+(defun tmr-mode-line--format-timer (timer)
+  "Format a single TIMER for display in the mode-line."
+  (propertize
+   (format-spec tmr-mode-line-format
+                `((?r . ,(tmr-mode-line--format-remaining timer))
+                  (?d . ,(tmr-mode-line--format-description timer))))
+   'help-echo (tmr--long-description timer)))
+
+(defun tmr-mode-line--get-active-timers ()
+  "Return a sorted list of active timers."
+  (thread-last tmr--timers
+               (seq-remove #'tmr--timer-finishedp)
+               (seq-sort-by #'tmr--timer-end-date #'time-less-p)))
+
+(defun tmr-mode-line--update ()
+  "Updates `tmr-mode-line-string' based on the current timer state."
+  (setq
+   tmr-mode-line-string
+   (if-let* ((active-timers (tmr-mode-line--get-active-timers)))
+       (let* ((truncate (and tmr-mode-line-max-timers
+                             (length> active-timers tmr-mode-line-max-timers)))
+              (timers-to-show (if truncate
+                                 (seq-take active-timers
+                                           tmr-mode-line-max-timers)
+                                active-timers)))
+         (concat
+          " " tmr-mode-line-prefix " "
+          (string-join (mapcar #'tmr-mode-line--format-timer timers-to-show)
+                       tmr-mode-line-separator)
+          (when truncate
+            (format " +%d" (- (length active-timers) 
tmr-mode-line-max-timers)))
+          " "))
+     "")))
+
+(defun tmr-mode-line-update ()
+  "Update the mode line with current timer information."
+  (tmr-mode-line--update)
+  (force-mode-line-update t))
+
+;;;###autoload
+(define-minor-mode tmr-mode-line-mode
+  "Display TMR May Ring timers in the global mode line."
+  :global t
+  :group 'tmr-mode-line
+  (if tmr-mode-line-mode
+      (progn
+        (unless global-mode-string (setq global-mode-string '("")))
+        (unless (memq 'tmr-mode-line-string global-mode-string)
+          (setq global-mode-string
+                (append global-mode-string '(tmr-mode-line-string))))
+        (setq tmr-mode-line--update-timer
+              (run-at-time t 1 #'tmr-mode-line-update))
+        (add-hook 'tmr--update-hook #'tmr-mode-line-update))
+    (when tmr-mode-line--update-timer
+      (cancel-timer tmr-mode-line--update-timer)
+      (setq tmr-mode-line--update-timer nil))
+    (setq tmr-mode-line-string nil)
+    (remove-hook 'tmr--update-hook #'tmr-mode-line-update)))
+
+(provide 'tmr-mode-line)
+;;; tmr-mode-line.el ends here

Reply via email to