From mboxrd@z Thu Jan 1 00:00:00 1970 Path: news.gmane.io!.POSTED.blaine.gmane.org!not-for-mail From: "J.P." Newsgroups: gmane.emacs.bugs Subject: bug#63569: 30.0.50; ERC 5.6: Add automatic nickname highlighting to ERC Date: Mon, 12 Jun 2023 21:07:22 -0700 Message-ID: <87r0qgknt1.fsf__15764.4733366155$1686629306$gmane$org@neverwas.me> References: <87ilcp1za1.fsf@neverwas.me> Mime-Version: 1.0 Content-Type: multipart/mixed; boundary="=-=-=" Injection-Info: ciao.gmane.io; posting-host="blaine.gmane.org:116.202.254.214"; logging-data="23868"; mail-complaints-to="usenet@ciao.gmane.io" User-Agent: Gnus/5.13 (Gnus v5.13) Cc: emacs-erc@gnu.org To: 63569@debbugs.gnu.org Original-X-From: bug-gnu-emacs-bounces+geb-bug-gnu-emacs=m.gmane-mx.org@gnu.org Tue Jun 13 06:08:18 2023 Return-path: Envelope-to: geb-bug-gnu-emacs@m.gmane-mx.org Original-Received: from lists.gnu.org ([209.51.188.17]) by ciao.gmane.io with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.92) (envelope-from ) id 1q8vKL-0005zO-VY for geb-bug-gnu-emacs@m.gmane-mx.org; Tue, 13 Jun 2023 06:08:18 +0200 Original-Received: from localhost ([::1] helo=lists1p.gnu.org) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1q8vKE-00032W-Up; Tue, 13 Jun 2023 00:08:12 -0400 Original-Received: from eggs.gnu.org ([2001:470:142:3::10]) by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1q8vK6-000311-IB for bug-gnu-emacs@gnu.org; Tue, 13 Jun 2023 00:08:05 -0400 Original-Received: from debbugs.gnu.org ([209.51.188.43]) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_128_GCM_SHA256:128) (Exim 4.90_1) (envelope-from ) id 1q8vK6-0008Kp-9e for bug-gnu-emacs@gnu.org; Tue, 13 Jun 2023 00:08:02 -0400 Original-Received: from Debian-debbugs by debbugs.gnu.org with local (Exim 4.84_2) (envelope-from ) id 1q8vK6-0003q7-4h for bug-gnu-emacs@gnu.org; Tue, 13 Jun 2023 00:08:02 -0400 X-Loop: help-debbugs@gnu.org Resent-From: "J.P." Original-Sender: "Debbugs-submit" Resent-CC: bug-gnu-emacs@gnu.org Resent-Date: Tue, 13 Jun 2023 04:08:02 +0000 Resent-Message-ID: Resent-Sender: help-debbugs@gnu.org X-GNU-PR-Message: followup 63569 X-GNU-PR-Package: emacs X-GNU-PR-Keywords: patch Original-Received: via spool by 63569-submit@debbugs.gnu.org id=B63569.168662926514724 (code B ref 63569); Tue, 13 Jun 2023 04:08:02 +0000 Original-Received: (at 63569) by debbugs.gnu.org; 13 Jun 2023 04:07:45 +0000 Original-Received: from localhost ([127.0.0.1]:41111 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1q8vJn-0003pM-8U for submit@debbugs.gnu.org; Tue, 13 Jun 2023 00:07:45 -0400 Original-Received: from mail-108-mta66.mxroute.com ([136.175.108.66]:38077) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1q8vJi-0003p2-R0 for 63569@debbugs.gnu.org; Tue, 13 Jun 2023 00:07:41 -0400 Original-Received: from mail-111-mta2.mxroute.com ([136.175.111.2] filter006.mxroute.com) (Authenticated sender: mN4UYu2MZsgR) by mail-108-mta66.mxroute.com (ZoneMTA) with ESMTPSA id 188b2efecf00002506.001 for <63569@debbugs.gnu.org> (version=TLSv1/SSLv3 cipher=ECDHE-RSA-AES128-GCM-SHA256); Tue, 13 Jun 2023 04:07:28 +0000 X-Zone-Loop: 75af81326cfcfeb26be699888f0ae62ceb4bb25bd36b X-Originating-IP: [136.175.111.2] DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=neverwas.me ; s=x; h=Content-Type:MIME-Version:Message-ID:Date:References:In-Reply-To: Subject:Cc:To:From:Sender:Reply-To:Content-Transfer-Encoding:Content-ID: Content-Description:Resent-Date:Resent-From:Resent-Sender:Resent-To:Resent-Cc :Resent-Message-ID:List-Id:List-Help:List-Unsubscribe:List-Subscribe: List-Post:List-Owner:List-Archive; bh=TNI/it1Qge0SgzCy1Q99w321Us1He8Rm7dsbDGZjIf4=; b=k4SpBiIcs4MmQjLnfMwnsCLICu 1HhAwvjxvSyRCr2/ZtNSB/CABHAj9rU4355JtgS8CO129TLle4BPoHwV3XhGbt7mNN8DBY3I6V9VG IlyBlxnxT48HZTVy9JIhAOwUwwe2VRcJWQZM7awavsUWs3sOeJt84vCINjWk57DDgP9CszNkr0icA oohLtbIDi1EFfdPrFGjnEXX0s078te28wHWdZo1mgTl03OgGfed9jWTY7YXIj0/HEa/zjL7g/r0mG 9fCpyekhde9exelVCCjbtI6dNlshMwGOaO0/ACCDmw/wpR5Kgs6X+dL8Fc60X+PEZryID07sKAdz8 pjOscWng==; In-Reply-To: <87ilcp1za1.fsf@neverwas.me> (J. P.'s message of "Thu, 18 May 2023 07:37:26 -0700") X-Authenticated-Id: masked@neverwas.me X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list X-BeenThere: bug-gnu-emacs@gnu.org List-Id: "Bug reports for GNU Emacs, the Swiss army knife of text editors" List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: bug-gnu-emacs-bounces+geb-bug-gnu-emacs=m.gmane-mx.org@gnu.org Original-Sender: bug-gnu-emacs-bounces+geb-bug-gnu-emacs=m.gmane-mx.org@gnu.org Xref: news.gmane.io gmane.emacs.bugs:263305 Archived-At: --=-=-= Content-Type: text/plain v4. Drop erc-buttons patch. Be smarter when looking for faces to skip. Just a minor update. As noted, I've dropped the first patch 0001-5.6-Allow-ERC-modules-to-extend-erc-nick-popup-alist.patch which has been installed on HEAD as e560f9af8e8 Allow ERC modules to extend erc-nick-popup-alist --=-=-= Content-Type: text/x-patch Content-Disposition: attachment; filename=0000-v3-v4.diff >From 5100a10672355255a80549acde1ca939f61e465d Mon Sep 17 00:00:00 2001 From: "F. Jason Park" Date: Mon, 12 Jun 2023 21:00:28 -0700 Subject: [PATCH 0/1] *** NOT A PATCH *** *** BLURB HERE *** David Leatherman (1): [5.6] Add module for colorizing nicknames to ERC doc/misc/erc.texi | 4 + etc/ERC-NEWS | 8 + lisp/erc/erc-nicks.el | 485 +++++++++++++++++++++++++++++++ lisp/erc/erc.el | 1 + test/lisp/erc/erc-nicks-tests.el | 340 ++++++++++++++++++++++ test/lisp/erc/erc-tests.el | 2 +- 6 files changed, 839 insertions(+), 1 deletion(-) create mode 100644 lisp/erc/erc-nicks.el create mode 100644 test/lisp/erc/erc-nicks-tests.el Interdiff: diff --git a/lisp/erc/erc-nicks.el b/lisp/erc/erc-nicks.el index 0e0a481d453..ad4fca523d2 100644 --- a/lisp/erc/erc-nicks.el +++ b/lisp/erc/erc-nicks.el @@ -34,7 +34,7 @@ ;; init.el. Customize users need only click "Apply and Save", as ;; usual. -;; History: +;;; History: ;; This module has enjoyed a number of contributors across several ;; variants over the years. To those not mentioned, your efforts are @@ -69,8 +69,9 @@ erc-nicks (defcustom erc-nicks-ignore-chars ",`'_-" "Trailing characters in a nick to ignore while highlighting. Value should be a string containing characters typically appended -by IRC clients a la `erc-nick-uniquifier' to secure a nickname -after a rejection. A value of nil means don't trim anything." +by IRC clients to secure a nickname after a rejection (see option +`erc-nick-uniquifier'). A value of nil means don't trim +anything." :type '(choice (string :tag "Chars to trim") (const :tag "Don't trim" nil))) @@ -95,7 +96,7 @@ erc-nicks-bg-color :type 'string) (defcustom erc-nicks-color-adjustments - '(erc-nicks-add-contrast erc-nicks-cap-contrast erc-nicks-ensaturate) + '(erc-nicks-invert erc-nicks-cap-contrast erc-nicks-ensaturate) "Treatments applied to improve aesthetics or visibility. For example, the function `erc-nicks-invert' inverts a nick when it's too close to the background, and `erc-nicks-add-contrast' @@ -110,7 +111,7 @@ erc-nicks-color-adjustments (function-item :tag "Bound saturation" erc-nicks-ensaturate) function))) -(defcustom erc-nicks-contrast-range '(4.0 . 12.5) +(defcustom erc-nicks-contrast-range '(4.3 . 12.5) "Desired range of contrast as a cons of (MIN . MAX). When `erc-nicks-add-contrast' and/or `erc-nicks-invert' appear in `erc-nicks-color-adjustments', MIN specifies the minimum amount @@ -118,9 +119,10 @@ erc-nicks-contrast-range foreground colors. Depending on the background, nicks may appear tinted in pastels or shaded with muted grays. MAX works similarly for reducing contrast, but only when -`erc-nicks-cap-contrast' is active. Values can range from 1.0 to -21.0(:1) but may produce unsatisfactory results toward either -extreme." +`erc-nicks-cap-contrast' is active. Users with lighter +backgrounds may want to lower MAX significantly. Either value +can range from 1.0 to 21.0(:1) but may produce unsatisfactory +results toward either extreme." :type '(cons float float)) (defcustom erc-nicks-saturation-range '(0.2 . 0.8) @@ -183,11 +185,14 @@ erc-nicks--bg-mode (defvar erc-nicks--grad-steps 9) ;; https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-contrast.html - +;; +;; TODO see implementation in https://elpa.gnu.org/packages/ement and +;; maybe copy that instead. (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)))) + ;; Shouldn't this use the actual bg color instead of b+w? (stop (if (eq (if decrease 'light 'dark) (erc-nicks--bg-mode)) '(1.0 1.0 1.0) '(0.0 0.0 0.0))) @@ -275,6 +280,17 @@ erc-nicks--hash n)) h)) +;; From https://elpa.gnu.org/packages/ement. The resolution has been +;; scaled up to try and avoid components being exactly 0.0, which our +;; contrast function doesn't seem to like. Hopefully, that's OK. +(defun erc-nicks--gen-color-ement (string) + "Generate normalized RGB color from STRING." + (let* ((ratio (/ (float (abs (sxhash string))) (float most-positive-fixnum))) + (color-num (round (* (* #xffff #xffff #xffff) ratio)))) + (list (/ (float (logand color-num #xffff)) #xffff) + (/ (float (ash (logand color-num #xffff0000) -16)) #xffff) + (/ (float (ash (logand color-num #xffff00000000) -32)) #xffff)))) + (defvar-local erc-nicks--colors-len nil) (defvar-local erc-nicks--custom-keywords '(:group erc-nicks :group erc-faces)) @@ -312,12 +328,13 @@ erc-nicks--redirect-face-widget-link (cddr args) plist)))) args) -(defun erc-nicks--reduce (color-string) - "Fold contrast strategies over COLOR-STRING." +(defun erc-nicks--reduce (color) + "Fold contrast strategies over COLOR, a string or normalized triple. +Return a hex string." (apply #'color-rgb-to-hex (seq-reduce (lambda (color strategy) (funcall strategy color)) erc-nicks-color-adjustments - (color-name-to-rgb color-string)))) + (if (stringp color) (color-name-to-rgb color) color)))) (defun erc-nicks--get-face (nick key) "Retrieve or create a face for NICK, stored locally under KEY. @@ -332,7 +349,7 @@ erc-nicks--get-face (erc-nicks--revive face face nick (erc-network)))))) (let ((color (erc-nicks--reduce (pcase erc-nicks-colors - ('all (format "#%012x" (erc-nicks--hash key))) + ('all (erc-nicks--gen-color-ement key)) ((or 'defined v) (unless v (setq v (defined-colors (selected-frame)))) (unless erc-nicks--colors-len @@ -345,6 +362,32 @@ erc-nicks--get-face new-face (format "Internal face for %s on %s." nick (erc-network))) (puthash nick new-face table))))) +(define-inline erc-nicks--anon-face-p (face) + (inline-quote (and (consp ,face) + (pcase (car ,face) + ((pred keywordp) t) + ('foreground-color t) + ('background-color t))))) + +(defvar erc-nicks--max-skip-search 3 ; make this an option? + "Max number of faces to visit when testing `erc-nicks-skip-faces'.") + +(defun erc-nicks--skip-p (prop option limit) + "Return non-nil if a face in PROP appears in OPTION. +But abandon search after examining LIMIT faces." + (setq prop (if (erc-nicks--anon-face-p prop) (list prop) (ensure-list prop))) + (catch 'found + (while-let (((> limit 0)) + (elem (pop prop))) + (while (and (consp elem) (not (erc-nicks--anon-face-p elem))) + (when (cdr elem) + (push (cdr elem) prop)) + (setq elem (car elem))) + (when elem + (cl-decf limit) + (when (if (symbolp elem) (memq elem option) (member elem option)) + (throw 'found elem)))))) + (defvar erc-nicks--phony-face nil "Face to pretend is propertizing the nick at point. Modules needing to colorize nicks outside of a buttonizing @@ -366,8 +409,8 @@ erc-nicks--highlight (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 + ((not (erc-nicks--skip-p face erc-nicks-skip-faces + erc-nicks--max-skip-search))) ;; 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) diff --git a/test/lisp/erc/erc-nicks-tests.el b/test/lisp/erc/erc-nicks-tests.el index e84a2fea6ce..0d640ad59c3 100644 --- a/test/lisp/erc/erc-nicks-tests.el +++ b/test/lisp/erc/erc-nicks-tests.el @@ -300,4 +300,41 @@ erc-nicks--hash (when noninteractive (kill-buffer)))) +(ert-deftest erc-nicks--skip-p () + ;; Baseline + (should-not (erc-nicks--skip-p 'bold nil 10000000)) + (should-not (erc-nicks--skip-p '(bold) nil 10000000)) + (should-not (erc-nicks--skip-p nil '(bold) 10000000)) + (should-not (erc-nicks--skip-p 'bold '(bold) 0)) + (should-not (erc-nicks--skip-p '(bold) '(bold) 0)) + (should-not (erc-nicks--skip-p 'bold '(foo bold) 0)) + (should-not (erc-nicks--skip-p '((:inherit bold)) '(bold) 1)) + (should (erc-nicks--skip-p 'bold '(bold) 1)) + (should (erc-nicks--skip-p 'bold '(fake bold) 1)) + (should (erc-nicks--skip-p 'bold '(foo bar bold) 1)) + (should (erc-nicks--skip-p '(bold) '(bold) 1)) + (should (erc-nicks--skip-p '((bold)) '(bold) 1)) + (should (erc-nicks--skip-p '((((bold)))) '(bold) 1)) + (should (erc-nicks--skip-p '(bold) '(foo bold) 1)) + (should (erc-nicks--skip-p '(:inherit bold) '((:inherit bold)) 1)) + (should (erc-nicks--skip-p '((:inherit bold)) '((:inherit bold)) 1)) + (should (erc-nicks--skip-p '(((:inherit bold))) '((:inherit bold)) 1)) + + ;; Composed + (should-not (erc-nicks--skip-p '(italic bold) '(bold) 1)) + (should-not (erc-nicks--skip-p '((italic) bold) '(bold) 1)) + (should-not (erc-nicks--skip-p '(italic (bold)) '(bold) 1)) + (should (erc-nicks--skip-p '(italic bold) '(bold) 2)) + (should (erc-nicks--skip-p '((italic) bold) '(bold) 2)) + (should (erc-nicks--skip-p '(italic (bold)) '(bold) 2)) + + (should-not (erc-nicks--skip-p '(italic default bold) '(bold) 2)) + (should-not (erc-nicks--skip-p '((default italic) bold) '(bold) 2)) + (should-not (erc-nicks--skip-p '(italic (default bold)) '(bold) 2)) + (should-not (erc-nicks--skip-p '((default italic) (bold shadow)) '(bold) 2)) + (should (erc-nicks--skip-p '((default italic) bold) '(bold) 3)) + (should (erc-nicks--skip-p '(italic (default bold)) '(bold) 3)) + (should (erc-nicks--skip-p '((default italic) (bold shadow)) '(bold) 3)) + (should (erc-nicks--skip-p '(italic (default (bold shadow))) '(bold) 3))) + ;;; erc-nicks-tests.el ends here -- 2.40.1 --=-=-= Content-Type: text/x-patch; charset=utf-8 Content-Disposition: attachment; filename=0001-5.6-Add-module-for-colorizing-nicknames-to-ERC.patch Content-Transfer-Encoding: quoted-printable >From 5100a10672355255a80549acde1ca939f61e465d Mon Sep 17 00:00:00 2001 From: David Leatherman 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-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) Co-authored-by: Andy Stewart --- doc/misc/erc.texi | 4 + etc/ERC-NEWS | 8 + lisp/erc/erc-nicks.el | 485 +++++++++++++++++++++++++++++++ lisp/erc/erc.el | 1 + test/lisp/erc/erc-nicks-tests.el | 340 ++++++++++++++++++++++ test/lisp/erc/erc-tests.el | 2 +- 6 files changed, 839 insertions(+), 1 deletion(-) 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 e848ed21a50..07484122e4b 100644 --- a/doc/misc/erc.texi +++ b/doc/misc/erc.texi @@ -459,6 +459,10 @@ Modules @item netsplit Detect netsplits =20 +@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 68f1083621c..d6383b72557 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. =20 +** A new module for nickname highlighting has joined ERC. +Automatic nickname coloring has come to ERC core. Users familiar with +'erc-hl-nicks', from which this module directly descends, 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-nicks.el b/lisp/erc/erc-nicks.el new file mode 100644 index 00000000000..ad4fca523d2 --- /dev/null +++ b/lisp/erc/erc-nicks.el @@ -0,0 +1,485 @@ +;;; erc-nicks.el -- Nick colors for ERC -*- lexical-binding: t; -*- + +;; Copyright (C) 2023 Free Software Foundation, Inc. + +;; Author: David Leatherman +;; Andy Stewart + +;; 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 file provides the `nicks' module for automatic nickname +;; highlighting. Add `nicks' to `erc-modules' to get started. +;; +;; To change the color of a nickname in a target buffer, click on it +;; and choose "Edit face" from the completion interface, and then +;; perform your adjustments in the resulting Customize menu. +;; Non-Customize users can persist their changes permanently by +;; clicking on the face's "location" hyperlink and copying the +;; generated code snippet (`defface' or `use-package') to their +;; init.el. Customize users need only click "Apply and Save", as +;; usual. + +;;; History: + +;; This module has enjoyed a number of contributors across several +;; variants over the years. To those not mentioned, your efforts are +;; no less appreciated. + +;; 2023/05 - erc-nicks +;; Rewrite using internal API, and rebrand for ERC 5.6 +;; 2020/03 - erc-hl-nicks 1.3.4 +;; Final release, see [1] for intervening history +;; 2014/05 - erc-highlight-nicknames.el +;; Final release, see [2] for intervening history +;; 2011/08 - erc-hl-nicks 1.0 +;; Initial release forked from erc-highlight-nicknames.el +;; 2008/12 - erc-highlight-nicknames.el +;; First release from Andy Stewart +;; 2007/09 - erc-highlight-nicknames.el +;; Initial release by by Andr=C3=A9 Riemann + +;; [1] +;; [2] + +;;; 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 ",`'_-" + "Trailing characters in a nick to ignore while highlighting. +Value should be a string containing characters typically appended +by IRC clients to secure a nickname after a rejection (see option +`erc-nick-uniquifier'). A value of nil means don't trim +anything." + :type '(choice (string :tag "Chars to trim") + (const :tag "Don't trim" nil))) + +(defcustom erc-nicks-skip-nicks nil + "Nicks to avoid highlighting." + :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-adjustments + '(erc-nicks-invert erc-nicks-cap-contrast erc-nicks-ensaturate) + "Treatments applied to improve aesthetics or visibility. +For example, the function `erc-nicks-invert' inverts a nick when +it's too close to the background, and `erc-nicks-add-contrast' +attempts to find a decent contrast ratio by brightening or +darkening. Note that ERC still applies adjustments when +`erc-nicks-colors' is a user-defined list of colors. Specify a +value of nil to prevent that." + :type '(repeat + (choice (function-item :tag "Invert" erc-nicks-invert) + (function-item :tag "Add contrast" erc-nicks-add-contras= t) + (function-item :tag "Cap contrast" erc-nicks-cap-contras= t) + (function-item :tag "Bound saturation" erc-nicks-ensatur= ate) + function))) + +(defcustom erc-nicks-contrast-range '(4.3 . 12.5) + "Desired range of contrast as a cons of (MIN . MAX). +When `erc-nicks-add-contrast' and/or `erc-nicks-invert' appear in +`erc-nicks-color-adjustments', MIN specifies the minimum amount +of contrast allowed between a buffer's background and its +foreground colors. Depending on the background, nicks may appear +tinted in pastels or shaded with muted grays. MAX works +similarly for reducing contrast, but only when +`erc-nicks-cap-contrast' is active. Users with lighter +backgrounds may want to lower MAX significantly. Either value +can range from 1.0 to 21.0(:1) but may produce unsatisfactory +results toward either extreme." + :type '(cons float float)) + +(defcustom erc-nicks-saturation-range '(0.2 . 0.8) + "Desired range for constraining saturation. +Expressed as a cons of decimal proportions. Only matters when +`erc-nicks-ensaturate' appears in `erc-nicks-color-adjustments'." + :type '(cons float float)) + +;; Should we also accept a list of faces? +(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. This is the +same as the Y component returned by `color-srgb-to-xyz'." + (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 (<=3D ch 0.04045) + (/ ch 12.92) + (expt (/ (+ ch 0.055) 1.055) 2.4))))))) + +(defvar-local erc-nicks--bg-luminance nil) + +(defun erc-nicks--get-contrast (fg &optional bg) + "Return a float between 1 and 21 for colors FG and BG. +If FG or BG are floats, interpret them as luminance values." + (let* ((lum-fg (if (numberp fg) fg (erc-nicks--get-luminance fg))) + (lum-bg (if bg + (if (numberp bg) bg (erc-nicks--get-luminance bg)) + (or erc-nicks--bg-luminance + (setq erc-nicks--bg-luminance + (erc-nicks--get-luminance erc-nicks-bg-color)= ))))) + (when (< lum-fg lum-bg) (cl-rotatef lum-fg lum-bg)) + (/ (+ 0.05 lum-fg) (+ 0.05 lum-bg)))) + +(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) + +;; https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-contra= st.html +;; +;; TODO see implementation in https://elpa.gnu.org/packages/ement and +;; maybe copy that instead. +(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)))) + ;; Shouldn't this use the actual bg color instead of b+w? + (stop (if (eq (if decrease 'light 'dark) (erc-nicks--bg-mode)) + '(1.0 1.0 1.0) + '(0.0 0.0 0.0))) + ;; From `color-gradient' in color.el + (r (nth 0 color)) + (g (nth 1 color)) + (b (nth 2 color)) + (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) + ;; FIXME stop when sufficiently close instead of exhausting. + (while (let* ((lum-fg (erc-nicks--get-luminance (list r g b))) + (darker (if (< lum-bg lum-fg) lum-bg lum-fg)) + (lighter (if (=3D 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))))) + (list 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-range'." + (erc-nicks--adjust-contrast color (car erc-nicks-contrast-range))) + +(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-range'." + (erc-nicks--adjust-contrast color (cdr erc-nicks-contrast-range) 'remove= )) + +(defun erc-nicks-invert (color) + "Invert COLOR based on the CAR of `erc-nicks-contrast-range'. +Don't bother if the inverted color has less contrast than the +input." + (if-let ((con-input (erc-nicks--get-contrast color)) + ((< con-input (car erc-nicks-contrast-range))) + (flipped (mapcar (lambda (c) (- 1.0 c)) color)) + ((> (erc-nicks--get-contrast flipped) con-input))) + flipped + color)) + +(defun erc-nicks-ensaturate (color) + "Ensure COLOR falls within `erc-nicks-saturation-range'." + (pcase-let ((`(,min . ,max) erc-nicks-saturation-range) + (`(,h ,s ,l) (apply #'color-rgb-to-hsl color))) + (cond ((> s max) (setq color (color-hsl-to-rgb h max l))) + ((< s min) (setq color (color-hsl-to-rgb h min l))))) + 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)) + +;; From https://elpa.gnu.org/packages/ement. The resolution has been +;; scaled up to try and avoid components being exactly 0.0, which our +;; contrast function doesn't seem to like. Hopefully, that's OK. +(defun erc-nicks--gen-color-ement (string) + "Generate normalized RGB color from STRING." + (let* ((ratio (/ (float (abs (sxhash string))) (float most-positive-fixn= um))) + (color-num (round (* (* #xffff #xffff #xffff) ratio)))) + (list (/ (float (logand color-num #xffff)) #xffff) + (/ (float (ash (logand color-num #xffff0000) -16)) #xffff) + (/ (float (ash (logand color-num #xffff00000000) -32)) #xffff)))) + +(defvar-local erc-nicks--colors-len nil) +(defvar-local erc-nicks--custom-keywords '(:group erc-nicks :group erc-fac= es)) + +(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 you= r\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 f= ace)) + (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--reduce (color) + "Fold contrast strategies over COLOR, a string or normalized triple. +Return a hex string." + (apply #'color-rgb-to-hex + (seq-reduce (lambda (color strategy) (funcall strategy color)) + erc-nicks-color-adjustments + (if (stringp color) (color-name-to-rgb color) color))= )) + +(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 (erc-nicks--reduce + (pcase erc-nicks-colors + ('all (erc-nicks--gen-color-ement 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-networ= k))) + (puthash nick new-face table))))) + +(define-inline erc-nicks--anon-face-p (face) + (inline-quote (and (consp ,face) + (pcase (car ,face) + ((pred keywordp) t) + ('foreground-color t) + ('background-color t))))) + +(defvar erc-nicks--max-skip-search 3 ; make this an option? + "Max number of faces to visit when testing `erc-nicks-skip-faces'.") + +(defun erc-nicks--skip-p (prop option limit) + "Return non-nil if a face in PROP appears in OPTION. +But abandon search after examining LIMIT faces." + (setq prop (if (erc-nicks--anon-face-p prop) (list prop) (ensure-list pr= op))) + (catch 'found + (while-let (((> limit 0)) + (elem (pop prop))) + (while (and (consp elem) (not (erc-nicks--anon-face-p elem))) + (when (cdr elem) + (push (cdr elem) prop)) + (setq elem (car elem))) + (when elem + (cl-decf limit) + (when (if (symbolp elem) (memq elem option) (member elem option)) + (throw 'found elem)))))) + +(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 + (string-trim-right (erc-server-user-nickname server-us= er) + (rx-to-string + `(: (+ (any ,erc-nicks-ignore-char= s)) + eot))) + (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-obj= ect)) + 'font-lock-face))) + ((not (erc-nicks--skip-p face erc-nicks-skip-faces + erc-nicks--max-skip-search))) + ;; 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-func= tion) + #'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 t= o \"" + 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-button--nick-popup-alist nil nil #'equ= al) + #'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-button--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 a1538962602..7b54b5db276 100644 --- a/lisp/erc/erc.el +++ b/lisp/erc/erc.el @@ -2017,6 +2017,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" nic= ks) (const :tag "noncommands: Don't display non-IRC commands after evaluat= ion" 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-tes= ts.el new file mode 100644 index 00000000000..0d640ad59c3 --- /dev/null +++ b/test/lisp/erc/erc-nicks-tests.el @@ -0,0 +1,340 @@ +;;; 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: + +;; Unlike most of ERC's tests, the ones in this file can be run +;; interactively in the same session. + +;; 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) + +;; This function replicates the behavior of older "invert" strategy +;; implementations from EmacsWiki, etc. The values for the lower and +;; upper bounds (0.33 and 0.66) are likewise inherited. See +;; `erc-nicks--invert-classic--dark' below for one reason its results +;; may not be plainly obvious. +(defun erc-nicks-tests--invert-classic (color) + (if (pcase (erc-nicks--bg-mode) + ('dark (< (erc-nicks--get-luminance color) (/ 1 3.0))) + ('light (> (erc-nicks--get-luminance color) (/ 2 3.0)))) + (list (- 1.0 (nth 0 color)) (- 1.0 (nth 1 color)) (- 1.0 (nth 2 colo= r))) + color)) + + +(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--classic () + (let ((convert (lambda (n) (apply #'color-rgb-to-hex + (erc-nicks-tests--invert-classic + (color-name-to-rgb n)))))) + (let ((erc-nicks--bg-mode-value 'dark)) + (should (equal (funcall convert "white") "#ffffffffffff")) + (should (equal (funcall convert "black") "#ffffffffffff")) + (should (equal (funcall convert "green") "#0000ffff0000"))) + (let ((erc-nicks--bg-mode-value 'light)) + (should (equal (funcall convert "white") "#000000000000")) + (should (equal (funcall convert "black") "#000000000000")) + (should (equal (funcall convert "green") "#ffff0000ffff"))))) + +(ert-deftest erc-nicks--get-contrast () + (should (=3D 21.0 (erc-nicks--get-contrast "white" "black"))) + (should (=3D 21.0 (erc-nicks--get-contrast "black" "white"))) + (should (=3D 1.0 (erc-nicks--get-contrast "black" "black"))) + (should (=3D 1.0 (erc-nicks--get-contrast "white" "white")))) + +(defun erc-nicks-tests--print-contrast (fn color) + (let* ((erc-nicks-color-adjustments (list fn)) + (result (erc-nicks--reduce color)) + (start (point))) + (insert (format "%16s%-16s%16s%-16s\n" + (concat color "-") + (concat ">" result) + (concat color " ") + (concat " " result))) + (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--invert-classic--light () + (let ((erc-nicks--bg-luminance 1.0) + (erc-nicks--bg-mode-value 'light) + (show (lambda (c) (erc-nicks-tests--print-contrast + #'erc-nicks-tests--invert-classic c)))) + + (with-current-buffer (get-buffer-create + "*erc-nicks--invert-classic--light*") + (should (equal "#000000000000" (funcall show "white"))) + (should (equal "#000000000000" (funcall show "black"))) + (should (equal "#ffff00000000" (funcall show "red"))) + (should (equal "#ffff0000ffff" (funcall show "green"))) ; magenta + (should (equal "#00000000ffff" (funcall show "blue"))) + + (unless noninteractive + (should (equal "#bbbbbbbbbbbb" (funcall show "#bbbbbbbbbbbb"))) + (should (equal "#cccccccccccc" (funcall show "#cccccccccccc"))) + (should (equal "#222122212221" (funcall show "#dddddddddddd"))) + (should (equal "#111011101110" (funcall show "#eeeeeeeeeeee")))) + + (when noninteractive + (kill-buffer))))) + +;; This shows that the output can be darker (have less contrast) than +;; the input. +(ert-deftest erc-nicks--invert-classic--dark () + (let ((erc-nicks--bg-luminance 0.0) + (erc-nicks--bg-mode-value 'dark) + (show (lambda (c) (erc-nicks-tests--print-contrast + #'erc-nicks-tests--invert-classic c)))) + + (with-current-buffer (get-buffer-create + "*erc-nicks--invert-classic--dark*") + (should (equal "#ffffffffffff" (funcall show "white"))) + (should (equal "#ffffffffffff" (funcall show "black"))) + (should (equal "#0000ffffffff" (funcall show "red"))) ; cyan + (should (equal "#0000ffff0000" (funcall show "green"))) + (should (equal "#ffffffff0000" (funcall show "blue"))) ; yellow + + (unless noninteractive + (should (equal "#aaaaaaaaaaaa" (funcall show "#555555555555"))) + (should (equal "#999999999999" (funcall show "#666666666666"))) + (should (equal "#888888888888" (funcall show "#777777777777"))) + (should (equal "#777777777777" (funcall show "#888888888888"))) + (should (equal "#666666666666" (funcall show "#999999999999"))) + (should (equal "#aaaaaaaaaaaa" (funcall show "#aaaaaaaaaaaa")))) + + (when noninteractive + (kill-buffer))))) + +;; These are the same as the legacy version but work in terms of +;; contrast ratios. Converting the original bounds to contrast ratios +;; (assuming pure white and black backgrounds) gives: +;; +;; min-lum of 0.33 ~~> 1.465 +;; max-lum of 0.66 ~~> 7.666 +;; +(ert-deftest erc-nicks-invert--light () + (let ((erc-nicks--bg-luminance 1.0) + (erc-nicks--bg-mode-value 'light) + (erc-nicks-contrast-range '(1.465)) + (show (lambda (c) (erc-nicks-tests--print-contrast + #'erc-nicks-invert c)))) + + (with-current-buffer (get-buffer-create + "*erc-nicks--invert-classic--light*") + (should (equal "#000000000000" (funcall show "white"))) + (should (equal "#000000000000" (funcall show "black"))) + (should (equal "#ffff00000000" (funcall show "red"))) + (should (equal "#ffff0000ffff" (funcall show "green"))) ; magenta + (should (equal "#00000000ffff" (funcall show "blue"))) + + (unless noninteractive + (should (equal "#bbbbbbbbbbbb" (funcall show "#bbbbbbbbbbbb"))) + (should (equal "#cccccccccccc" (funcall show "#cccccccccccc"))) + (should (equal "#222122212221" (funcall show "#dddddddddddd"))) + (should (equal "#111011101110" (funcall show "#eeeeeeeeeeee")))) + + (when noninteractive + (kill-buffer))))) + +(ert-deftest erc-nicks-invert--dark () + (let ((erc-nicks--bg-luminance 0.0) + (erc-nicks--bg-mode-value 'dark) + (erc-nicks-contrast-range '(7.666)) + (show (lambda (c) (erc-nicks-tests--print-contrast + #'erc-nicks-invert c)))) + + (with-current-buffer (get-buffer-create "*erc-nicks-invert--dark*") + (should (equal "#ffffffffffff" (funcall show "white"))) + (should (equal "#ffffffffffff" (funcall show "black"))) + (should (equal "#0000ffffffff" (funcall show "red"))) ; cyan + (should (equal "#0000ffff0000" (funcall show "green"))) + (should (equal "#ffffffff0000" (funcall show "blue"))) ; yellow + + (unless noninteractive + (should (equal "#aaaaaaaaaaaa" (funcall show "#555555555555"))) + (should (equal "#999999999999" (funcall show "#666666666666"))) + (should (equal "#888888888888" (funcall show "#777777777777"))) + (should (equal "#888888888888" (funcall show "#888888888888"))) + (should (equal "#999999999999" (funcall show "#999999999999")))) + + (when noninteractive + (kill-buffer))))) + +(ert-deftest erc-nicks-add-contrast () + (let ((erc-nicks--bg-luminance 1.0) + (erc-nicks--bg-mode-value 'light) + (erc-nicks-contrast-range '(3.5)) + (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 (=3D 12.5 (cdr erc-nicks-contrast-range))) + (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 -> 1= 2.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 (=3D (erc-nicks--hash "00000000") #xe4deaa6df385)) + (should (=3D (erc-nicks--hash "00000001") #xe4deaa6df386)) + (funcall show "#e4deaa6df385") + (funcall show "#e4deaa6df386") + + ;; So we currently pad from the right to avoid this. + (should (=3D (erc-nicks--hash "0Libera.Chat") #x32fdc0d63a92)) + (should (=3D (erc-nicks--hash "1Libera.Chat") #xc2c4f1c997f3)) + (funcall show "#32fdc0d63a92") + (funcall show "#c2c4f1c997f3") + + (should (=3D (erc-nicks--hash "0 OFTC") #x6805b7521261)) + (should (=3D (erc-nicks--hash "1 OFTC") #xf7cce8456fc2)) + (funcall show "#6805b7521261") + (funcall show "#f7cce8456fc2")) + + (when noninteractive + (kill-buffer)))) + +(ert-deftest erc-nicks--skip-p () + ;; Baseline + (should-not (erc-nicks--skip-p 'bold nil 10000000)) + (should-not (erc-nicks--skip-p '(bold) nil 10000000)) + (should-not (erc-nicks--skip-p nil '(bold) 10000000)) + (should-not (erc-nicks--skip-p 'bold '(bold) 0)) + (should-not (erc-nicks--skip-p '(bold) '(bold) 0)) + (should-not (erc-nicks--skip-p 'bold '(foo bold) 0)) + (should-not (erc-nicks--skip-p '((:inherit bold)) '(bold) 1)) + (should (erc-nicks--skip-p 'bold '(bold) 1)) + (should (erc-nicks--skip-p 'bold '(fake bold) 1)) + (should (erc-nicks--skip-p 'bold '(foo bar bold) 1)) + (should (erc-nicks--skip-p '(bold) '(bold) 1)) + (should (erc-nicks--skip-p '((bold)) '(bold) 1)) + (should (erc-nicks--skip-p '((((bold)))) '(bold) 1)) + (should (erc-nicks--skip-p '(bold) '(foo bold) 1)) + (should (erc-nicks--skip-p '(:inherit bold) '((:inherit bold)) 1)) + (should (erc-nicks--skip-p '((:inherit bold)) '((:inherit bold)) 1)) + (should (erc-nicks--skip-p '(((:inherit bold))) '((:inherit bold)) 1)) + + ;; Composed + (should-not (erc-nicks--skip-p '(italic bold) '(bold) 1)) + (should-not (erc-nicks--skip-p '((italic) bold) '(bold) 1)) + (should-not (erc-nicks--skip-p '(italic (bold)) '(bold) 1)) + (should (erc-nicks--skip-p '(italic bold) '(bold) 2)) + (should (erc-nicks--skip-p '((italic) bold) '(bold) 2)) + (should (erc-nicks--skip-p '(italic (bold)) '(bold) 2)) + + (should-not (erc-nicks--skip-p '(italic default bold) '(bold) 2)) + (should-not (erc-nicks--skip-p '((default italic) bold) '(bold) 2)) + (should-not (erc-nicks--skip-p '(italic (default bold)) '(bold) 2)) + (should-not (erc-nicks--skip-p '((default italic) (bold shadow)) '(bold)= 2)) + (should (erc-nicks--skip-p '((default italic) bold) '(bold) 3)) + (should (erc-nicks--skip-p '(italic (default bold)) '(bold) 3)) + (should (erc-nicks--skip-p '((default italic) (bold shadow)) '(bold) 3)) + (should (erc-nicks--skip-p '(italic (default (bold shadow))) '(bold) 3))) + +;;; erc-nicks-tests.el ends here diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el index f3489a16386..ebe49bcece2 100644 --- a/test/lisp/erc/erc-tests.el +++ b/test/lisp/erc/erc-tests.el @@ -2057,7 +2057,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 netsp= lit - 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)) =20 --=20 2.40.1 --=-=-=--