From looking into this, it's possible for a buffer local minor-mode to manage a global timer.
The global timer is disabled when the buffer-local mode becomes inactive and is re-enabled when it becomes active.
There is additional logic to ensure a buffer that becomes inactive isn't left in a stale state too.
This is done using a global idle timer and window-state-change-hook
to track changes to the active buffer.
The logic to manage timers is under the section Internal Timer Management
.
;;; my-hl-line.el --- Highlight the current line (example package) -*- lexical-binding: t -*-
;; ---------------------------------------------------------------------------
;; Custom Variables
(defgroup my-hl-line nil "Highlight the current line, as an example." :group 'faces)
(defcustom my-hl-line-idle-time 0.1
"Time after which to highlight the line."
:group 'my-hl-line
:type 'float)
;; ---------------------------------------------------------------------------
;; Internal Functions
;;
;; This is sample code, it highlights a line.
(defsubst my-hl-line--do-highlight-clear ()
"Clear current highlight."
(remove-overlays (point-min) (point-max) 'my-hl-line t))
(defun my-hl-line--do-highlight-set ()
"Highlight the current line."
(my-hl-line--do-highlight-clear)
(pcase-let* ((`(,beg . ,end) (bounds-of-thing-at-point 'line)))
(let ((ov (make-overlay beg end)))
(overlay-put ov 'face '(:inverse-video t :extend t))
(overlay-put ov 'my-hl-line t))))
;; ---------------------------------------------------------------------------
;; Internal Timer Management
;;
;; This works as follows:
;;
;; - The timer is kept active as long as the local mode is enabled.
;; - Entering a buffer runs the buffer local `window-state-change-hook'
;; immediately which checks if the mode is enabled,
;; set up the global timer if it is.
;; - Switching any other buffer wont run this hook,
;; rely on the idle timer it's self running, which detects the active mode,
;; canceling it's self if the mode isn't active.
;;
;; This is a reliable way of using a global,
;; repeating idle timer that is effectively buffer local.
;;
;; Global idle timer (repeating), keep active while the buffer-local mode is enabled.
(defvar my-hl-line--global-timer nil)
;; When t, the timer will update buffers in all other visible windows.
(defvar my-hl-line--dirty-flush-all nil)
;; When true, the buffer should be updated when inactive.
(defvar-local my-hl-line--dirty nil)
(defun my-hl-line--time-callback-or-disable ()
"Callback that run the repeat timer."
;; Ensure all other buffers are highlighted on request.
(let ((is-mode-active (bound-and-true-p my-hl-line-mode)))
;; When this buffer is not in the mode, flush all other buffers.
(cond
(is-mode-active
;; Don't update in the window loop to ensure we always
;; update the current buffer in the current context.
(setq my-hl-line--dirty nil))
(t
;; If the timer ran when in another buffer,
;; a previous buffer may need a final refresh, ensure this happens.
(setq my-hl-line--dirty-flush-all t)))
(when my-hl-line--dirty-flush-all
;; Run the mode callback for all other buffers in the queue.
(dolist (frame (frame-list))
(dolist (win (window-list frame -1))
(let ((buf (window-buffer win)))
(when
(and
(buffer-local-value 'my-hl-line-mode buf)
(buffer-local-value 'my-hl-line--dirty buf))
(with-selected-frame frame
(with-selected-window win
(with-current-buffer buf
(setq my-hl-line--dirty nil)
(my-hl-line--do-highlight-set)))))))))
;; Always keep the current buffer dirty
;; so navigating away from this buffer will refresh it.
(if is-mode-active
(setq my-hl-line--dirty t))
(cond
(is-mode-active
(my-hl-line--do-highlight-set))
(t ;; Cancel the timer until the current buffer uses this mode again.
(my-hl-line--time-ensure nil)))))
(defun my-hl-line--time-ensure (state)
"Ensure the timer is enabled when STATE is non-nil, otherwise disable."
(cond
(state
(unless my-hl-line--global-timer
(setq my-hl-line--global-timer
(run-with-idle-timer
my-hl-line-idle-time
:repeat 'my-hl-line--time-callback-or-disable))))
(t
(when my-hl-line--global-timer
(cancel-timer my-hl-line--global-timer)
(setq my-hl-line--global-timer nil)))))
(defun my-hl-line--time-reset ()
"Run this when the buffer changes."
;; Ensure changing windows doesn't leave other buffers with stale highlight.
(cond
((bound-and-true-p my-hl-line-mode)
(setq my-hl-line--dirty-flush-all t)
(setq my-hl-line--dirty t)
(my-hl-line--time-ensure t))
(t
(my-hl-line--time-ensure nil))))
(defun my-hl-line--time-buffer-local-enable ()
"Ensure buffer local state is enabled."
;; Needed in case focus changes before the idle timer runs.
(setq my-hl-line--dirty-flush-all t)
(setq my-hl-line--dirty t)
(my-hl-line--time-ensure t)
(add-hook 'window-state-change-hook #'my-hl-line--time-reset nil t))
(defun my-hl-line--time-buffer-local-disable ()
"Ensure buffer local state is disabled."
(kill-local-variable 'my-hl-line--dirty)
(my-hl-line--time-ensure nil)
(remove-hook 'window-state-change-hook #'my-hl-line--time-reset t))
;; ---------------------------------------------------------------------------
;; Public Functions
;;;###autoload
(define-minor-mode my-hl-line-mode
"Idle-Highlight Minor Mode."
:group 'my-hl-line
:global nil
(cond
(my-hl-line-mode
(my-hl-line--time-buffer-local-enable))
(t
(my-hl-line--time-buffer-local-disable)
(my-hl-line--do-highlight-clear))))
(provide 'my-hl-line-mode)
;;; my-hl-line-mode.el ends here