;;; mode-line-bell.el --- Flash the mode line instead of ringing the bell  -*- lexical-binding: t; -*-

;; Copyright (C) 2018, 2026  Steve Purcell

;; Author: Steve Purcell <steve@sanityinc.com>
;; Package-Version: 0.3
;; Package-Revision: 6ab459f7be31
;; Keywords: convenience

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <https://www.gnu.org/licenses/>.

;;; Commentary:

;; Enable the global minor mode `mode-line-bell-mode' to set
;; `ring-bell-function' to a function that will briefly flash the mode
;; line when the bell is rung.

;;; Code:

(defgroup mode-line-bell nil
  "Flash the mode line instead of ringing the bell."
  :group 'frames)

(defcustom mode-line-bell-flash-time 0.05
  "Length of time to flash the mode line when the bell is rung."
  :type 'float
  :safe 'floatp)

(defvar mode-line-bell--flashing nil
  "If non-nil, the mode line is currently flashing.")

(defconst mode-line-bell--face
  (if (facep 'mode-line-active) 'mode-line-active 'mode-line)
  "Face whose `:inverse-video' attribute is toggled while flashing.

Since Emacs 29, the selected window's mode line is drawn with
`mode-line-active' rather than `mode-line'.")

(defun mode-line-bell--begin-flash ()
  "Begin flashing the mode line."
  (unless mode-line-bell--flashing
    (set-face-attribute mode-line-bell--face nil :inverse-video
                        (not (face-attribute mode-line-bell--face :inverse-video nil 'default)))
    (setq mode-line-bell--flashing t)))

(defun mode-line-bell--end-flash (inverse-video)
  "Finish flashing the mode line.
INVERSE-VIDEO is the original state of the :inverse-video attribute on
`mode-line-bell--face'."
  (when mode-line-bell--flashing
    (set-face-attribute mode-line-bell--face nil :inverse-video inverse-video)
    (setq mode-line-bell--flashing nil)))

;;;###autoload
(defun mode-line-bell-flash ()
  "Flash the mode line momentarily."
  (unless mode-line-bell--flashing
    (let ((saved-inverse-video (face-attribute mode-line-bell--face :inverse-video)))
      (run-with-timer mode-line-bell-flash-time nil 'mode-line-bell--end-flash saved-inverse-video)
      (mode-line-bell--begin-flash))))

;;;###autoload
(define-minor-mode mode-line-bell-mode
  "Flash the mode line instead of ringing the bell."
  :lighter nil
  :global t
  (setq-default ring-bell-function (when mode-line-bell-mode
                                     'mode-line-bell-flash)))


(provide 'mode-line-bell)
;;; mode-line-bell.el ends here
