From 8e16d161b6e9f3c67b4ccbe9e44fc73c43bb70f5 Mon Sep 17 00:00:00 2001 From: "F. Jason Park" Date: Sun, 18 Dec 2022 19:01:40 -0800 Subject: [PATCH 1/1] [5.6] Add module for colorizing nicknames to ERC * doc/misc/erc.texi: Add `nicks' to module lineup. * etc/ERC-NEWS: Mention new module `nicks'. * lisp/erc/erc-button.el (erc--nick-popup-alist, erc-nick-popup): New variable to help the latter access special actions owned by modules. * lisp/erc/erc-nicks.el: New file. * lisp/erc/erc.el: (erc-modules): Add `nicks'. * test/lisp/erc/erc-nicks-tests.el: New file. * test/lisp/erc/erc-tests (erc-tests--modules): Add `nicks'. (Bug#63569) --- doc/misc/erc.texi | 4 + etc/ERC-NEWS | 8 + lisp/erc/erc-button.el | 12 +- lisp/erc/erc-nicks.el | 389 +++++++++++++++++++++++++++++++ lisp/erc/erc.el | 1 + test/lisp/erc/erc-nicks-tests.el | 174 ++++++++++++++ test/lisp/erc/erc-tests.el | 2 +- 7 files changed, 586 insertions(+), 4 deletions(-) create mode 100644 lisp/erc/erc-nicks.el create mode 100644 test/lisp/erc/erc-nicks-tests.el diff --git a/doc/misc/erc.texi b/doc/misc/erc.texi index 1f343fc8529..f7036e57638 100644 --- a/doc/misc/erc.texi +++ b/doc/misc/erc.texi @@ -459,6 +459,10 @@ Modules @item netsplit Detect netsplits +@cindex modules, nicks +@item nicks +Automatically colorize nicks + @cindex modules, noncommands @item noncommands Don't display non-IRC commands after evaluation diff --git a/etc/ERC-NEWS b/etc/ERC-NEWS index d257bdcbf51..2d3626e28b8 100644 --- a/etc/ERC-NEWS +++ b/etc/ERC-NEWS @@ -30,6 +30,14 @@ helper called 'erc-fill-wrap-nudge' allows for dynamic "refilling" of buffers on the fly. Set 'erc-fill-function' to 'erc-fill-wrap' to get started. +** A new module for nickname highlighting has joined ERC. +Automatic nickname coloring has come to ERC core. Users familiar with +the excellent 'erc-hl-nicks' by David Leatherman, from which this new +addition draws heavily, will already be familiar with its suite of +handy options. By default, each nickname in an ERC session receives a +unique face with a unique (or evenly dealt) foreground color. Add +'nicks' to 'erc-modules' to get started. + ** A unified interactive entry point. New users are often dismayed to discover that M-x ERC doesn't connect to its default network, Libera.Chat, over TLS. Though perhaps a diff --git a/lisp/erc/erc-button.el b/lisp/erc/erc-button.el index 4307dc3b860..b370fa4bf95 100644 --- a/lisp/erc/erc-button.el +++ b/lisp/erc/erc-button.el @@ -685,15 +685,21 @@ erc-nick-popup-alist :type '(repeat (cons (string :tag "Op") sexp))) +(defvar-local erc--nick-popup-alist nil + "Internally controlled items for `erc-nick-popup-alist'.") + (defun erc-nick-popup (nick) (let* ((completion-ignore-case t) + (alist (append erc-nick-popup-alist erc--nick-popup-alist)) (action (completing-read (format-message "What action to take on `%s'? " nick) - erc-nick-popup-alist)) - (code (cdr (assoc action erc-nick-popup-alist)))) + alist)) + (code (cdr (assoc action alist)))) (when code (erc-set-active-buffer (current-buffer)) - (eval code `((nick . ,nick)))))) + (if (functionp code) + (funcall code nick) + (eval code `((nick . ,nick))))))) ;;; Callback functions (defun erc-button-describe-symbol (symbol-name) diff --git a/lisp/erc/erc-nicks.el b/lisp/erc/erc-nicks.el new file mode 100644 index 00000000000..85d182f9a09 --- /dev/null +++ b/lisp/erc/erc-nicks.el @@ -0,0 +1,389 @@ +;;; erc-nicks.el -- Nick colors for ERC -*- lexical-binding: t; -*- + +;; Copyright (C) 2023 Free Software Foundation, Inc. + +;; This file is part of GNU Emacs. + +;; GNU Emacs 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. + +;; GNU Emacs 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 GNU Emacs. If not, see . + +;;; Commentary: + +;; This module is heavily influenced by the lovely and more featureful +;; +;; `erc-hl-nicks' by David Leatherman +;; +;; +;; which itself is based on +;; +;; `erc-highlight-nicknames' by André Riemann, Andy Stewart, and +;; others +;; + +;;; Code: + +(require 'erc-button) +(require 'color) + +(defgroup erc-nicks nil + "Colorize nicknames in ERC buffers." + :package-version '(ERC . "5.6") ; FIXME sync on release + :group 'erc) + +(defcustom erc-nicks-ignore-chars-regexp + (cons (rx bot (+ (any ",`'_-"))) (rx (+ (any ",`'_-")) eot)) + "Characters surrounding a nick to ignore while highlighting. +Regexps should be suitable for `string-trim'." + :type '(choice (cons regexp regexp) (const nil))) + +(defcustom erc-nicks-skip-nicks nil + "Nicks to not highlight." + :type '(repeat string)) + +(defcustom erc-nicks-skip-faces '(erc-notice-face + erc-current-nick-face erc-my-nick-face + erc-pal-face erc-fool-face) + "Faces to avoid highlighting atop." + :type '(repeat symbol)) + +(defcustom erc-nicks-nickname-face erc-button-nickname-face + "Face to mix with generated one for emphasizing non-speakers." + :type '(choice face (const nil))) + +(defcustom erc-nicks-bg-color + (frame-parameter (selected-frame) 'background-color) + "Background color for calculating contrast. +Set this explicitly when the background color isn't discoverable, +which may be the case in terminal Emacs." + :type 'string) + +(defcustom erc-nicks-color-contrast-strategy + '(erc-nicks-add-contrast erc-nicks-cap-contrast) + "Treatments applied to colors for increasing visibility. +A value of `erc-nicks-invert' inverts a nick when it's too close +to the background. A value of `erc-nicks-add-contrast' +attempts to find a decent contrast ratio by brightening or +darkening. This option can also be a list, in which case, +members will be applied in the order they appear. For example, + + \\='(erc-nicks-invert erc-nicks-add-contrast) + +will invert as needed and likewise adjust the contrast. Note +that anything specified by this option will still be applied when +`erc-nicks-colors' is a user-defined list of colors." + :type '(choice (function-item :tag "Invert" erc-nicks-invert) + (function-item :tag "Add contrast" erc-nicks-add-contrast) + (function-item :tag "Cap contrast" erc-nicks-cap-contrast) + (repeat function) + (const nil) + function)) + +(defcustom erc-nicks-contrast-ratio '(3.5 . 12.5) + "Desired range of contrast as a cons of (MIN . MAX). +For this to matter, `erc-nicks-color-contrast-strategy' must be +set to `erc-nicks-add-contrast' or `erc-nicks-cap-contrast' or +contain at least one if that option is a list. If adding +contrast, MIN specifies the minimum amount allowed between a +buffer's background color and the foreground colors specified by +`erc-nicks-colors'. The closer the number to the possible +maximum of 21(:1), the greater the contrast. Depending on the +background, nicks are either tinted in pastel or muted with dark +gray. MAX works similarly for reducing contrast." + :type '(cons float float)) + +(defcustom erc-nicks-colors 'all + "Pool of colors. +This can be a list of hexes or color names, such as those +provided by `defined-colors', which can itself be used when the +value is the symbol `defined'. With `all', use any 24-bit color." + :type '(choice (const all) (const defined) (list string))) + +(defvar-local erc-nicks--face-table nil + "Hash table containing unique nick faces.") + +;; https://stackoverflow.com/questions/596216#answer-56678483 +(defun erc-nicks--get-luminance (color) + "Return relative luminance of COLOR. +COLOR can be a list of normalized values or a name." + (let ((out 0) + (coefficients '(0.2126 0.7152 0.0722)) + (chnls (if (stringp color) (color-name-to-rgb color) color))) + (dolist (ch chnls out) + (cl-incf out (* (pop coefficients) + (if (<= ch 0.04045) + (/ ch 12.92) + (expt (/ (+ ch 0.055) 1.055) 2.4))))))) + +(defvar-local erc-nicks--bg-mode-value nil) + +(defmacro erc-nicks--bg-mode () + `(or erc-nicks--bg-mode-value + (setq erc-nicks--bg-mode-value + ,(cond ((fboundp 'frame--current-background-mode) + '(frame--current-background-mode (selected-frame))) + ((fboundp 'frame--current-backround-mode) + '(frame--current-backround-mode (selected-frame))) + (t + '(frame-parameter (selected-frame) 'background-mode)))))) + +(defvar erc-nicks--grad-steps 9) +(defvar-local erc-nicks--bg-luminance nil) + +;; https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-contrast.html +;; +;; We could cache results, which may help when `erc-nicks-colors' is +;; set to `defined'. + +(defun erc-nicks--adjust-contrast (color target &optional decrease) + (let* ((lum-bg (or erc-nicks--bg-luminance + (setq erc-nicks--bg-luminance + (erc-nicks--get-luminance erc-nicks-bg-color)))) + (stop (if (eq (if decrease 'light 'dark) (erc-nicks--bg-mode)) + '(1.0 1.0 1.0) + '(0.0 0.0 0.0))) + (start (color-name-to-rgb color)) + ;; From `color-gradient' in color.el + (r (nth 0 start)) + (g (nth 1 start)) + (b (nth 2 start)) + (interval (float (1+ (expt 2 erc-nicks--grad-steps)))) + (r-step (/ (- (nth 0 stop) r) interval)) + (g-step (/ (- (nth 1 stop) g) interval)) + (b-step (/ (- (nth 2 stop) b) interval)) + (maxtries erc-nicks--grad-steps) + started) + (while (let* ((lum-fg (erc-nicks--get-luminance (list r g b))) + (darker (if (< lum-bg lum-fg) lum-bg lum-fg)) + (lighter (if (= darker lum-bg) lum-fg lum-bg)) + (cur (/ (+ 0.05 lighter) (+ 0.05 darker))) + (scale (expt 2 maxtries))) + (cond ((if decrease (> cur target) (< cur target)) + (setq r (+ r (* r-step scale)) + g (+ g (* g-step scale)) + b (+ b (* b-step scale)))) + (started + (setq r (- r (* r-step scale)) + g (- g (* g-step scale)) + b (- b (* b-step scale)))) + (t (setq maxtries 1))) + (unless started + (setq started t)) + (setq r (min 1.0 (max 0 r)) + g (min 1.0 (max 0 g)) + b (min 1.0 (max 0 b))) + (not (zerop (cl-decf maxtries))))) + (color-rgb-to-hex r g b))) + +(defun erc-nicks-add-contrast (color) + "Increase COLOR's contrast by blending it with white or black. +Unless sufficient contrast exists between COLOR and the +background, raise it to somewhere around the lower bound of +`erc-nicks-contrast-ratio'." + (erc-nicks--adjust-contrast color (car erc-nicks-contrast-ratio))) + +(defun erc-nicks-cap-contrast (color) + "Reduce COLOR's contrast by blending it with white or black. +If excessive contrast exists between COLOR and the background, +lower it to the upper bound of `erc-nicks-contrast-ratio'." + (erc-nicks--adjust-contrast color (cdr erc-nicks-contrast-ratio) 'remove)) + +;; Inversion thresholds for dark and light, respectively. +(defvar erc-nicks--min-lum (/ 1 3.0)) +(defvar erc-nicks--max-lum (/ 2 3.0)) + +(defun erc-nicks-invert (color) + "Invert COLOR based on luminance and background." + (if (pcase (erc-nicks--bg-mode) + ('dark (< (erc-nicks--get-luminance color) erc-nicks--min-lum)) + ('light (> (erc-nicks--get-luminance color) erc-nicks--max-lum))) + (pcase-let ((`(,r ,g ,b) (color-values color))) + (format "#%04x%04x%04x" (- 65535 r) (- 65535 g) (- 65535 b))) + color)) + +;; http://www.cse.yorku.ca/~oz/hash.html +;; See also gui_nick_hash_djb2_64 in weechat/src/gui/gui-nick.c, +;; which is originally from https://savannah.nongnu.org/patch/?8062. +;; +;; Short strings of the same length and those differing only in their +;; low order bits tend to land in neighboring buckets, which are often +;; similar in color. Padding on the right with at least nine added +;; chars seems to scramble things sufficiently enough for our needs. + +(defun erc-nicks--hash (s &optional nchoices) + (let ((h 5381) ; seed and multiplier (33) hardcoded for now + (p (or nchoices 281474976710656)) ; 48-bits (expt 2 48) + (i 0) + (n (length s))) + (while (< (setq h (% (+ (* h 33) (aref s i)) p) + i (1+ i)) + n)) + h)) + +(defvar-local erc-nicks--colors-len nil) +(defvar-local erc-nicks--custom-keywords '(:group erc-nicks :group erc-faces)) + +(defun erc-nicks--revive (new-face old-face nick net) + (put new-face 'erc-nicks--custom-nick (cons nick net)) + (apply #'custom-declare-face new-face (face-user-default-spec old-face) + (format "Persistent `erc-nicks' color for %s on %s." nick net) + erc-nicks--custom-keywords)) + +(defun erc-nicks--create-defface-template (face) + (pop-to-buffer (get-buffer-create (format "*New face %s*" face))) + (erase-buffer) + (lisp-interaction-mode) + (insert ";; If you *don't* use Customize, put something like this in your\n" + (substitute-command-keys + ";; init.el and use \\[eval-last-sexp] to apply any edits.\n\n") + (format "(defface %s\n '%S\n %S" + face (face-user-default-spec face) (face-documentation face)) + (cl-loop for (k v) on erc-nicks--custom-keywords by #'cddr + concat (format "\n %s %S" k (list 'quote v))) + ")\n\n;; Or, if you use use-package\n(use-package erc-nicks\n" + " :custom-face\n" + (format " (%s %S)" face (face-user-default-spec face)) + ")\n")) + +(defun erc-nicks--redirect-face-widget-link (args) + (pcase args + (`(,widget face-link . ,plist) + (when-let* ((face (widget-value widget)) + ((get face 'erc-nicks--custom-nick))) + (unless (symbol-file face) + (setf (plist-get plist :action) + (lambda (&rest _) (erc-nicks--create-defface-template face)))) + (setf (plist-get plist :help-echo) "Create or edit `defface'." + (cddr args) plist)))) + args) + +(defun erc-nicks--get-face (nick key) + "Retrieve or create a face for NICK, stored locally under KEY. +But favor a custom erc-nicks-NICK@NETWORK-face, when defined." + (setq nick (erc-downcase nick)) + (let ((table (buffer-local-value 'erc-nicks--face-table + (erc-server-buffer)))) + (or (gethash nick table) + (and-let* ((face (intern-soft (concat "erc-nicks-" nick "@" + (erc-network-name) "-face"))) + ((or (and (facep face) face) + (erc-nicks--revive face face nick (erc-network)))))) + (let ((color (seq-reduce + (lambda (color strategy) (funcall strategy color)) + (erc-list erc-nicks-color-contrast-strategy) + (pcase erc-nicks-colors + ('all (format "#%012x" (erc-nicks--hash key))) + ((or 'defined v) + (unless v (setq v (defined-colors (selected-frame)))) + (unless erc-nicks--colors-len + (setq erc-nicks--colors-len (length v))) + (nth (erc-nicks--hash key erc-nicks--colors-len) + v))))) + (new-face (make-symbol (concat "erc-nicks-" nick "-face")))) + (face-spec-set new-face `((t :foreground ,color)) 'face-defface-spec) + (set-face-documentation + new-face (format "Internal face for %s on %s." nick (erc-network))) + (puthash nick new-face table))))) + +(defvar erc-nicks--phony-face nil + "Face to pretend is propertizing the nick at point. +Modules needing to colorize nicks outside of a buttonizing +context can use this instead of setting fictitious bounds on the +`erc-button--nick' object passed to `erc-nicks--highlight'.") + +(defun erc-nicks--highlight (nick-object) + "Possibly highlight a single nick." + (when-let* + ((nick-object) + (server-user (erc-button--nick-user nick-object)) + (trimmed (if erc-nicks-ignore-chars-regexp + (string-trim (erc-server-user-nickname server-user) + (car erc-nicks-ignore-chars-regexp) + (cdr erc-nicks-ignore-chars-regexp)) + (erc-server-user-nickname server-user))) + ((not (member trimmed erc-nicks-skip-nicks))) + (face (or erc-nicks--phony-face + (get-text-property (car (erc-button--nick-bounds nick-object)) + 'font-lock-face))) + ((not (seq-some (lambda (f) (memq f erc-nicks-skip-faces)) + (erc-list face)))) ; cl-notany + ;; Ensure nicks are colored uniquely (per network) by padding + ;; from the right, as mentioned above in `erc-nicks--hash'. + (key (concat (erc-button--nick-downcased nick-object) + (and-let* ((net (erc-network))) (format "%9s" net)))) + (out (erc-nicks--get-face trimmed key))) + ;; `font-lock-prepend-text-property' could also work if preserving + ;; history isn't needed (in which case this var should be nil). + (setf (erc-button--nick-erc-button-nickname-face nick-object) + (if (or (not erc-nicks-nickname-face) + (eq face erc-nicks-nickname-face)) + out + (cons out (erc-list erc-nicks-nickname-face))))) + nick-object) + +(define-erc-module nicks nil + "Uniquely colorize nicknames in target buffers." + ((if erc--target + (progn + (add-function :filter-return (local 'erc-button--modify-nick-function) + #'erc-nicks--highlight '((depth . 80))) + (erc-button--phantom-users-mode +1)) + (unless erc-button-mode + (unless (memq 'button erc-modules) + (erc--warn-once-before-connect 'erc-nicks-mode + "Enabling default global module `button' needed by local" + " module `nicks'. This will impact \C-]all\C-] ERC" + " sessions. Add `nicks' to `erc-modules' to avoid this" + " warning. See Info:\"(erc) Modules\" for more.")) + (erc-button-mode +1)) + (when (equal erc-nicks-bg-color "unspecified-bg") + (let ((temp (if (eq (erc-nicks--bg-mode) 'light) "white" "black"))) + (erc-button--display-error-notice-with-keys + "Module `nicks' unable to determine background color. Setting to \"" + temp "\" globally. Please see `erc-nicks-bg-color'.") + (custom-set-variables (list 'erc-nicks-bg-color temp)))) + (setq erc-nicks--face-table (make-hash-table :test #'equal))) + (setf (alist-get "Edit face" erc--nick-popup-alist nil nil #'equal) + #'erc-nicks-customize-face) + (advice-add 'widget-create-child-and-convert :filter-args + #'erc-nicks--redirect-face-widget-link)) + ((kill-local-variable 'erc-nicks--face-table) + (kill-local-variable 'erc-nicks--bg-mode-value) + (kill-local-variable 'erc-nicks--bg-luminance) + (kill-local-variable 'erc-nicks--colors-len) + (when (fboundp 'erc-button--phantom-users-mode) + (erc-button--phantom-users-mode -1)) + (remove-function (local 'erc-button--modify-nick-function) + #'erc-nicks--highlight) + (setf (alist-get "Edit face" erc--nick-popup-alist nil 'remove #'equal) + nil)) + 'local) + +(defun erc-nicks-customize-face (nick) + "Customize or create persistent face for NICK." + (interactive (list (or (car (get-text-property (point) 'erc-data)) + (completing-read "nick: " (or erc-channel-users + erc-server-users))))) + (setq nick (erc-downcase (substring-no-properties nick))) + (let* ((net (erc-network)) + (key (concat nick (and net (format "%9s" net)))) + (old-face (erc-nicks--get-face nick key)) + (new-face (intern (format "erc-nicks-%s@%s-face" nick net)))) + (unless (eq new-face old-face) + (erc-nicks--revive new-face old-face nick net) + (set-face-attribute old-face nil :foreground 'unspecified) + (set-face-attribute old-face nil :inherit new-face)) + (customize-face new-face))) + +(provide 'erc-nicks) + +;;; erc-nicks.el ends here diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el index 495e25212ce..5a3b312b53b 100644 --- a/lisp/erc/erc.el +++ b/lisp/erc/erc.el @@ -2026,6 +2026,7 @@ erc-modules move-to-prompt) (const :tag "netsplit: Detect netsplits" netsplit) (const :tag "networks: Provide data about IRC networks" networks) + (const :tag "nicks: Uniquely colorize nicknames in target buffers" nicks) (const :tag "noncommands: Don't display non-IRC commands after evaluation" noncommands) (const :tag "notifications: Desktop alerts on PRIVMSG or mentions" diff --git a/test/lisp/erc/erc-nicks-tests.el b/test/lisp/erc/erc-nicks-tests.el new file mode 100644 index 00000000000..e0a5691b073 --- /dev/null +++ b/test/lisp/erc/erc-nicks-tests.el @@ -0,0 +1,174 @@ +;;; erc-nicks-tests.el --- Tests for erc-nicks -*- lexical-binding:t -*- + +;; Copyright (C) 2023 Free Software Foundation, Inc. + +;; This file is part of GNU Emacs. + +;; GNU Emacs 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. + +;; GNU Emacs 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 GNU Emacs. If not, see . + +;;; Commentary: + +;; TODO: +;; +;; * Add mock session (or scenario) with buffer snapshots, like those +;; in erc-fill-tests.el. (Should probably move helpers to a common +;; library under ./resources.) + +;;; Code: + +(require 'ert) +(require 'erc-nicks) + +(ert-deftest erc-nicks--get-luminance () + (should (eql 0.0 (erc-nicks--get-luminance "black"))) + (should (eql 1.0 (erc-nicks--get-luminance "white"))) + (should (eql 21.0 (/ (+ 0.05 1.0) (+ 0.05 0.0)))) + + ;; RGB floats from a `display-graphic-p' session. + (let ((a (erc-nicks--get-luminance ; #9439ad + '(0.5803921568627451 0.2235294117647059 0.6784313725490196))) + (b (erc-nicks--get-luminance ; #ae54c7 + '(0.6823529411764706 0.32941176470588235 0.7803921568627451))) + (c (erc-nicks--get-luminance ; #d19ddf + '(0.8196078431372549 0.615686274509804 0.8745098039215686))) + (d (erc-nicks--get-luminance ; #f5e8f8 + '(0.9607843137254902 0.9098039215686274 0.9725490196078431)))) + ;; Low, med, high contrast comparisons against known values from + ;; an external source. + (should (eql 1.42 (/ (round (* 100 (/ (+ 0.05 b) (+ 0.05 a)))) 100.0))) + (should (eql 2.78 (/ (round (* 100 (/ (+ 0.05 c) (+ 0.05 a)))) 100.0))) + (should (eql 5.16 (/ (round (* 100 (/ (+ 0.05 d) (+ 0.05 a)))) 100.0))))) + +(ert-deftest erc-nicks-invert () + (let ((erc-nicks--bg-mode-value 'dark)) + (should (equal (erc-nicks-invert "white") "white")) + (should (equal (erc-nicks-invert "black") "#ffffffffffff")) + (should (equal (erc-nicks-invert "green") "green"))) + (let ((erc-nicks--bg-mode-value 'light)) + (should (equal (erc-nicks-invert "white") "#000000000000")) + (should (equal (erc-nicks-invert "black") "black")) + (should (equal (erc-nicks-invert "green") "#ffff0000ffff")))) + +(defun erc-nicks-tests--print-contrast (fn color) + (let ((result (funcall fn color)) + (fg (if (eq 'dark erc-nicks--bg-mode-value) "white" "black")) + (start (point))) + (insert (format "%16s%-16s%16s%-16s\n" + (concat color "-") + (concat ">" result) + (concat color " ") + (concat " " result))) + (put-text-property start (+ start 32) 'face + (list :foreground fg)) + (put-text-property (+ start 32) (+ start 48) 'face + (list :background color :foreground result)) + (put-text-property (+ start 48) (+ start 64) 'face + (list :background result :foreground color)) + result)) + +(ert-deftest erc-nicks-add-contrast () + (let ((erc-nicks--bg-luminance 1.0) + (erc-nicks--bg-mode-value 'light) + (show (lambda (c) (erc-nicks-tests--print-contrast + #'erc-nicks-add-contrast c)))) + + (with-current-buffer (get-buffer-create "*erc-nicks-add-contrast*") + (should (equal "#893a893a893a" (funcall show "white"))) + (should (equal "#893a893a893a" (funcall show "#893a893a893a"))) + (should (equal "#000000000000" (funcall show "black"))) + (should (equal "#ffff00000000" (funcall show "red"))) + (should (equal "#0000a12e0000" (funcall show "green"))) + (should (equal "#00000000ffff" (funcall show "blue"))) + + ;; When the input is already near the desired ratio, the result + ;; may not be in bounds, only close. But the difference is + ;; usually imperceptible. + (unless noninteractive + ;; Well inside (light slate gray) + (should (equal "#777788889999" (funcall show "#777788889999"))) + ;; Slightly outside -> just outside + (should (equal "#7c498bd39b5c" (funcall show "#88889999aaaa"))) + ;; Just outside -> just inside + (should (equal "#7bcc8b479ac0" (funcall show "#7c498bd39b5c"))) + ;; Just inside + (should (equal "#7bcc8b479ac0" (funcall show "#7bcc8b479ac0")))) + + (when noninteractive + (kill-buffer))))) + +(ert-deftest erc-nicks-cap-contrast () + (should (= 12.5 (cdr erc-nicks-contrast-ratio))) + (let ((erc-nicks--bg-luminance 1.0) + (erc-nicks--bg-mode-value 'light) + (show (lambda (c) (erc-nicks-tests--print-contrast + #'erc-nicks-cap-contrast c)))) + + (with-current-buffer (get-buffer-create "*erc-nicks-remove-contrast*") + (should (equal (funcall show "black") "#34e534e534e5" )) ; 21.0 -> 12.14 + (should ; 12.32 -> 12.32 (same) + (equal (funcall show "#34e534e534e5") "#34e534e534e5")) + (should (equal (funcall show "white") "#ffffffffffff")) + + (unless noninteractive + (should (equal (funcall show "DarkRed") "#8b8b00000000")) + (should (equal (funcall show "DarkGreen") "#000064640000")) + ;; 15.29 -> 12.38 + (should (equal (funcall show "DarkBlue") "#1cf11cf198b5")) + + ;; 12.50 -> 12.22 + (should (equal (funcall show "#33e033e033e0") "#34ab34ab34ab")) + ;; 12.57 -> 12.28 + (should (equal (funcall show "#338033803380") "#344c344c344c")) + ;; 12.67 -> 12.37 + (should (equal (funcall show "#330033003300") "#33cc33cc33cc"))) + + (when noninteractive + (kill-buffer))))) + +;; Here is an example of how filters can steer us wrong (don't always +;; DTRT). Two keys with similar names hash to very different values: +;; +;; 1) "awbLibera.Chat" -> #x1e3b5ca4edbc ; deep blue +;; 2) "twbLibera.Chat" -> #xdeb4c26934af ; yellow/orange +;; +;; But on a dark bg, (1) falls below `erc-nicks-invert's min threshold +;; and thus gets treated, becoming #xe1c4a35b1243, which is quite +;; close to and thus easily confused with (2). + +(ert-deftest erc-nicks--hash () + (with-current-buffer (get-buffer-create "*erc-nicks--hash*") + ;; Here, we're just using `erc-nicks-tests--show-contrast' for show. + (let ((show (lambda (c) (erc-nicks-tests--print-contrast #'identity c)))) + + ;; Similar nicks yielding similar colors is likely undesirable. + (should (= (erc-nicks--hash "00000000") #xe4deaa6df385)) + (should (= (erc-nicks--hash "00000001") #xe4deaa6df386)) + (funcall show "#e4deaa6df385") + (funcall show "#e4deaa6df386") + + ;; So we currently pad from the right to avoid this. + (should (= (erc-nicks--hash "0Libera.Chat") #x32fdc0d63a92)) + (should (= (erc-nicks--hash "1Libera.Chat") #xc2c4f1c997f3)) + (funcall show "#32fdc0d63a92") + (funcall show "#c2c4f1c997f3") + + (should (= (erc-nicks--hash "0 OFTC") #x6805b7521261)) + (should (= (erc-nicks--hash "1 OFTC") #xf7cce8456fc2)) + (funcall show "#6805b7521261") + (funcall show "#f7cce8456fc2")) + + (when noninteractive + (kill-buffer)))) + +;;; erc-nicks-tests.el ends here diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el index b624186d88d..991bfa3b082 100644 --- a/test/lisp/erc/erc-tests.el +++ b/test/lisp/erc/erc-tests.el @@ -1953,7 +1953,7 @@ erc-handle-irc-url (defconst erc-tests--modules '( autoaway autojoin button capab-identify completion dcc fill identd imenu irccontrols keep-place list log match menu move-to-prompt netsplit - networks noncommands notifications notify page readonly + networks nicks noncommands notifications notify page readonly replace ring sasl scrolltobottom services smiley sound spelling stamp track truncate unmorse xdcc)) -- 2.40.0