From a11f554fd533f2139cf6b9e592388a5385d4462b Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Mon, 11 May 2026 04:37:13 -0500 Subject: feat: make modeline status-message strings customizable The validation banner, retry-attempt message, async-failure tooltip, and the initial loading tooltip were hardcoded English literals. I added defcustoms for them so the wording can be localized or adjusted. The format-string ones keep their %d/%s directives, with the order spelled out in each docstring. I also dropped the dead :info severity from chime-validate-configuration's docstring. The function only ever emitted :ok, :warning, and :error. --- chime.el | 76 +++++++++++-- tests/test-chime-status-messages.el | 167 +++++++++++++++++++++++++++++ tests/test-chime-validate-configuration.el | 2 +- 3 files changed, 235 insertions(+), 10 deletions(-) create mode 100644 tests/test-chime-status-messages.el diff --git a/chime.el b/chime.el index c9ac491..cfe7446 100644 --- a/chime.el +++ b/chime.el @@ -634,6 +634,64 @@ The single format argument is the option name to customize." :group 'chime :type 'string) +(defcustom chime-validating-message "Chime: Validating configuration..." + "Banner printed to *Messages* when validation runs interactively." + :package-version '(chime . "0.8.0") + :group 'chime + :type 'string) + +(defcustom chime-validation-summary-format "Chime: %d error%s, %d warning%s." + "Format string for the validation summary line in *Messages*. +Receives four arguments: error count, error plural suffix, warning count, +warning plural suffix." + :package-version '(chime . "0.8.0") + :group 'chime + :type 'string) + +(defcustom chime-async-failure-tooltip + "Event check failed — check *Messages* buffer" + "Modeline tooltip shown when an async event check fails." + :package-version '(chime . "0.8.0") + :group 'chime + :type 'string) + +(defcustom chime-validation-errors-message + "Chime: Configuration errors detected (see *Messages* buffer for details)" + "Banner printed to *Messages* after validation has exhausted retries." + :package-version '(chime . "0.8.0") + :group 'chime + :type 'string) + +(defcustom chime-validation-error-tooltip + "Configuration error — check *Messages* buffer" + "Modeline tooltip shown after validation has exhausted retries." + :package-version '(chime . "0.8.0") + :group 'chime + :type 'string) + +(defcustom chime-validation-waiting-message-format + "Chime: Waiting for org-agenda-files to load... (attempt %d/%d)" + "Format string for the validation-retry banner in *Messages*. +Receives two arguments: current attempt number and the configured maximum." + :package-version '(chime . "0.8.0") + :group 'chime + :type 'string) + +(defcustom chime-validation-waiting-tooltip-format + "Waiting for org-agenda-files... (attempt %d/%d)" + "Format string for the validation-retry modeline tooltip. +Receives two arguments: current attempt number and the configured maximum." + :package-version '(chime . "0.8.0") + :group 'chime + :type 'string) + +(defcustom chime-modeline-initial-tooltip + "Chime: waiting for first event check..." + "Modeline tooltip shown before the first event check completes." + :package-version '(chime . "0.8.0") + :group 'chime + :type 'string) + (defcustom chime-sound-file (expand-file-name "sounds/chime.wav" (file-name-directory @@ -2000,7 +2058,7 @@ MESSAGE is nil for passing checks and contains issue details otherwise." "Display full validation RESULTS in the *Messages* buffer." (let ((errors 0) (warnings 0)) - (message "Chime: Validating configuration...") + (message "%s" chime-validating-message) (dolist (result results) (pcase-let ((`(,severity ,description ,detail) result)) (pcase severity @@ -2014,7 +2072,7 @@ MESSAGE is nil for passing checks and contains issue details otherwise." description) (when detail (message " %s" detail)))) - (message "Chime: %d error%s, %d warning%s." + (message chime-validation-summary-format errors (if (= errors 1) "" "s") warnings @@ -2024,7 +2082,7 @@ MESSAGE is nil for passing checks and contains issue details otherwise." (defun chime-validate-configuration () "Validate chime's runtime environment and configuration. Returns a list of (SEVERITY MESSAGE) pairs, or nil if all checks pass. -SEVERITY is one of: :error :warning :info +SEVERITY is one of: :error :warning Checks performed: - org-agenda-files is set and non-empty @@ -2116,7 +2174,7 @@ persistent-failure warning, and switches the modeline to its error state." (chime--debug-log-async-error err)) (chime--log-silently "Chime: %s: %s" prefix (error-message-string err)) (chime--maybe-warn-persistent-failures) - (chime--set-modeline-error-state "Event check failed — check *Messages* buffer")) + (chime--set-modeline-error-state chime-async-failure-tooltip)) (defun chime--handle-async-success (callback events) "Process a successful async fetch. Invoke CALLBACK with EVENTS. @@ -2179,15 +2237,15 @@ Returns nil if validation failed and check should be skipped." (dolist (err errors) (chime--log-silently "") (chime--log-silently "ERROR: %s" (cadr err)))) - (message "Chime: Configuration errors detected (see *Messages* buffer for details)") + (message "%s" chime-validation-errors-message) ;; Update modeline tooltip to show error state - (chime--set-modeline-error-state "Configuration error — check *Messages* buffer")) - (message "Chime: Waiting for org-agenda-files to load... (attempt %d/%d)" + (chime--set-modeline-error-state chime-validation-error-tooltip)) + (message chime-validation-waiting-message-format chime--validation-retry-count chime--validation-max-retries) ;; Update modeline tooltip to show waiting state (chime--set-modeline-error-state - (format "Waiting for org-agenda-files... (attempt %d/%d)" + (format chime-validation-waiting-tooltip-format chime--validation-retry-count chime--validation-max-retries))) nil) @@ -2252,7 +2310,7 @@ Uses `chime-modeline-no-events-text' with a loading tooltip." (let ((map (make-sparse-keymap))) (define-key map [mode-line mouse-1] #'chime--open-calendar-url) (propertize chime-modeline-no-events-text - 'help-echo "Chime: waiting for first event check..." + 'help-echo chime-modeline-initial-tooltip 'mouse-face 'mode-line-highlight 'local-map map)))) diff --git a/tests/test-chime-status-messages.el b/tests/test-chime-status-messages.el new file mode 100644 index 0000000..b15b6e4 --- /dev/null +++ b/tests/test-chime-status-messages.el @@ -0,0 +1,167 @@ +;;; test-chime-status-messages.el --- Tests for customizable status messages -*- lexical-binding: t; -*- + +;; Copyright (C) 2026 Craig Jennings + +;; Author: Craig Jennings + +;; 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 . + +;;; Commentary: + +;; Verify that the status-message defcustoms control the strings shown to +;; the user in *Messages* and in the modeline help-echo. Existing tests +;; in test-chime-validate-configuration.el and test-chime-validation-retry.el +;; cover the default text; this file proves the wiring honors customization. +;; +;; Mocks are kept in a single cl-letf per test to avoid native-comp +;; trampoline issues with nested redefinitions of subrs like `message'. + +;;; Code: + +(require 'test-bootstrap (expand-file-name "test-bootstrap.el")) +(require 'cl-lib) + +(defmacro test-chime-status--with-validation-state (&rest body) + "Run BODY with isolated chime validation/modeline state." + (declare (indent 0) (debug t)) + `(let ((chime--validation-done nil) + (chime--validation-retry-count 0) + (chime--validation-max-retries 3) + (chime--consecutive-async-failures 0) + (chime-modeline-string nil) + (chime-modeline-no-events-text " ⏰")) + ,@body)) + +(defun test-chime-status--tooltip-string (modeline-string) + "Return the `help-echo' property from MODELINE-STRING, or nil." + (and modeline-string + (get-text-property 0 'help-echo modeline-string))) + +;;; Interactive validation banner and summary + +(ert-deftest test-chime-validating-message-uses-defcustom () + "Normal: custom validating banner appears in *Messages* during interactive validate." + (let ((chime-validating-message "CUSTOM validating banner") + (org-agenda-files '("/tmp/exists.org")) + (chime-enable-modeline t) + (global-mode-string '("")) + (messages nil)) + (cl-letf (((symbol-function 'file-exists-p) (lambda (_) t)) + ((symbol-function 'require) (lambda (_ &optional _ _) t)) + ((symbol-function 'called-interactively-p) (lambda (_) t)) + ((symbol-function 'message) + (lambda (format-string &rest args) + (push (apply #'format format-string args) messages)))) + (chime-validate-configuration)) + (should (member "CUSTOM validating banner" (nreverse messages))))) + +(ert-deftest test-chime-validation-summary-format-uses-defcustom () + "Normal: custom summary format controls the count summary in *Messages*." + (let ((chime-validation-summary-format "summary errs=%d%s warns=%d%s") + (org-agenda-files '("/tmp/exists.org")) + (chime-enable-modeline t) + (global-mode-string '("")) + (messages nil)) + (cl-letf (((symbol-function 'file-exists-p) (lambda (_) t)) + ((symbol-function 'require) (lambda (_ &optional _ _) t)) + ((symbol-function 'called-interactively-p) (lambda (_) t)) + ((symbol-function 'message) + (lambda (format-string &rest args) + (push (apply #'format format-string args) messages)))) + (chime-validate-configuration)) + (should (member "summary errs=0s warns=0s" (nreverse messages))))) + +;;; Async failure tooltip + +(ert-deftest test-chime-async-failure-tooltip-uses-defcustom () + "Normal: async-failure tooltip uses the configured string." + (test-chime-status--with-validation-state + (let ((chime-async-failure-tooltip "CUSTOM async fail") + (chime-max-consecutive-failures 0)) + (chime--record-async-failure '(error "boom") "Async error") + (should (string-match-p + "CUSTOM async fail" + (test-chime-status--tooltip-string chime-modeline-string)))))) + +;;; Validation retry: errors-message + error tooltip (retries exhausted) + +(ert-deftest test-chime-validation-errors-message-uses-defcustom () + "Normal: errors-exhausted banner uses the configured string." + (test-chime-status--with-validation-state + (let ((chime-validation-errors-message "CUSTOM errors detected") + (org-agenda-files nil) + (messages nil)) + (setq chime--validation-retry-count 99) + (setq chime--validation-max-retries 0) + (cl-letf (((symbol-function 'message) + (lambda (format-string &rest args) + (push (apply #'format format-string args) messages)))) + (chime--maybe-validate)) + (should (member "CUSTOM errors detected" (nreverse messages)))))) + +(ert-deftest test-chime-validation-error-tooltip-uses-defcustom () + "Normal: errors-exhausted tooltip uses the configured string." + (test-chime-status--with-validation-state + (let ((chime-validation-error-tooltip "CUSTOM error tip") + (org-agenda-files nil)) + (setq chime--validation-retry-count 99) + (setq chime--validation-max-retries 0) + (cl-letf (((symbol-function 'message) (lambda (&rest _) nil))) + (chime--maybe-validate)) + (should (string-match-p + "CUSTOM error tip" + (test-chime-status--tooltip-string chime-modeline-string)))))) + +;;; Validation retry: waiting-message + waiting-tooltip (retry in progress) + +(ert-deftest test-chime-validation-waiting-message-format-uses-defcustom () + "Normal: retry banner uses the configured format with attempt/max." + (test-chime-status--with-validation-state + (let ((chime-validation-waiting-message-format "waiting %d of %d") + (org-agenda-files nil) + (messages nil)) + (setq chime--validation-retry-count 0) + (setq chime--validation-max-retries 3) + (cl-letf (((symbol-function 'message) + (lambda (format-string &rest args) + (push (apply #'format format-string args) messages)))) + (chime--maybe-validate)) + (should (member "waiting 1 of 3" (nreverse messages)))))) + +(ert-deftest test-chime-validation-waiting-tooltip-format-uses-defcustom () + "Normal: retry tooltip uses the configured format with attempt/max." + (test-chime-status--with-validation-state + (let ((chime-validation-waiting-tooltip-format "wait %d %d") + (org-agenda-files nil)) + (setq chime--validation-retry-count 0) + (setq chime--validation-max-retries 3) + (cl-letf (((symbol-function 'message) (lambda (&rest _) nil))) + (chime--maybe-validate)) + (should (string-match-p + "wait 1 3" + (test-chime-status--tooltip-string chime-modeline-string)))))) + +;;; Initial modeline tooltip (before first check) + +(ert-deftest test-chime-modeline-initial-tooltip-uses-defcustom () + "Normal: initial modeline tooltip uses the configured string." + (let ((chime-modeline-initial-tooltip "CUSTOM initial tip") + (chime-modeline-no-events-text " ⏰")) + (should (string-match-p + "CUSTOM initial tip" + (test-chime-status--tooltip-string + (chime--make-initial-modeline-string)))))) + +(provide 'test-chime-status-messages) +;;; test-chime-status-messages.el ends here diff --git a/tests/test-chime-validate-configuration.el b/tests/test-chime-validate-configuration.el index c971632..680808c 100644 --- a/tests/test-chime-validate-configuration.el +++ b/tests/test-chime-validate-configuration.el @@ -49,7 +49,7 @@ (cl-some (lambda (issue) (eq (car issue) :warning)) issues)) (defun test-chime-validate-configuration--count-issues (issues severity) - "Count number of ISSUES with given SEVERITY (:error, :warning, or :info)." + "Count number of ISSUES with given SEVERITY (:error or :warning)." (length (cl-remove-if-not (lambda (i) (eq (car i) severity)) issues))) ;;; Normal Cases - Valid Configurations -- cgit v1.2.3