diff options
Diffstat (limited to 'lisp/erc/erc.el')
-rw-r--r-- | lisp/erc/erc.el | 4186 |
1 files changed, 3223 insertions, 963 deletions
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el index 88e41e96a82..0750463a4e7 100644 --- a/lisp/erc/erc.el +++ b/lisp/erc/erc.el @@ -12,11 +12,14 @@ ;; David Edmondson (dme@dme.org) ;; Michael Olson (mwolson@gnu.org) ;; Kelvin White (kwhite@gnu.org) -;; Version: 5.5.0.29.1 -;; Package-Requires: ((emacs "27.1") (compat "29.1.3.4")) +;; Version: 5.6-git +;; Package-Requires: ((emacs "27.1") (compat "29.1.4.4")) ;; Keywords: IRC, chat, client, Internet ;; URL: https://www.gnu.org/software/emacs/erc.html +;; This is a GNU ELPA :core package. Avoid functionality that is not +;; compatible with the version of Emacs recorded above. + ;; This file is part of GNU Emacs. ;; GNU Emacs is free software: you can redistribute it and/or modify @@ -58,20 +61,16 @@ ;;; Code: -(load "erc-loaddefs" 'noerror 'nomessage) +(eval-and-compile (load "erc-loaddefs" 'noerror 'nomessage)) (require 'erc-networks) (require 'erc-backend) (require 'cl-lib) (require 'format-spec) -(require 'pp) -(require 'thingatpt) (require 'auth-source) -(require 'time-date) -(require 'iso8601) -(eval-when-compile (require 'subr-x) (require 'url-parse)) +(eval-when-compile (require 'subr-x)) -(defconst erc-version "5.5.0.29.1" +(defconst erc-version "5.6-git" "This version of ERC.") (defvar erc-official-location @@ -87,7 +86,8 @@ ("5.3" . "23.1") ("5.4" . "28.1") ("5.4.1" . "29.1") - ("5.5" . "29.1"))) + ("5.5" . "29.1") + ("5.6" . "30.1"))) (defgroup erc nil "Emacs Internet Relay Chat client." @@ -101,7 +101,9 @@ :group 'erc) (defgroup erc-display nil - "Settings for how various things are displayed." + "Settings controlling how various things are displayed. +See the customization group `erc-buffers' for display options +concerning buffers." :group 'erc) (defgroup erc-mode-line-and-header nil @@ -133,12 +135,82 @@ "Running scripts at startup and with /LOAD." :group 'erc) -;; Forward declarations -(defvar erc-message-parsed) +;; Add `custom-loads' features for group symbols missing from a +;; supported Emacs version, possibly because they belong to a new ERC +;; library. These groups all share their library's feature name. +;;;###autoload(dolist (symbol '( erc-sasl erc-spelling ; 29 +;;;###autoload erc-imenu erc-nicks)) ; 30 +;;;###autoload (custom-add-load symbol symbol)) + +(defvar erc-message-parsed) ; only known to this file + +(defvar erc--msg-props nil + "Hash table containing metadata properties for current message. +Provided by the insertion functions `erc-display-message' and +`erc-display-msg' while running their modification hooks. +Initialized when null for each visitation round from function +parameters and environmental factors, as well as the alist +`erc--msg-prop-overrides'. Keys are symbols. Values are opaque +objects, unless otherwise specified. Items present after running +`erc-insert-post-hook' or `erc-send-post-hook' become text +properties added to the first character of an inserted message. +A given message therefore spans the interval extending from one +set of such properties to the newline before the next (or +`erc-insert-marker'). As of ERC 5.6, this forms the basis for +visiting and editing inserted messages. Modules should align +their markers accordingly. The following properties have meaning +as of ERC 5.6: + + - `erc--msg': a symbol, guaranteed present; possible values + include `unknown', a fallback used by `erc-display-message'; a + catalog key, such as `s401' or `finished'; an + `erc-display-message' TYPE parameter, like `notice' + + - `erc--cmd': a message's associated IRC command, as read by + `erc--get-eq-comparable-cmd'; currently either a symbol, like + `PRIVMSG', or a number, like 5, which represents the numeric + \"005\"; absent on \"local\" messages, such as simple warnings + and help text, and on outgoing messages unless echoed back by + the server (assuming future support) + + - `erc--spkr': a string, the nick of the person speaking + + - `erc--ctcp': a CTCP command, like `ACTION' + + - `erc--ts': a timestamp, possibly provided by the server; as of + 5.6, a ticks/hertz pair on Emacs 29 and above, and a \"list\" + type otherwise; managed by the `stamp' module + + - `erc--ephemeral': a symbol prefixed by or matching a module + name; indicates to other modules and members of modification + hooks that the current message should not affect stateful + operations, such as recording a channel's most recent speaker + +This is an internal API, and the selection of related helper +utilities is fluid and provisional. As of ERC 5.6, see the +functions `erc--check-msg-prop' and `erc--get-inserted-msg-prop'.") + +(defvar erc--msg-prop-overrides nil + "Alist of \"message properties\" for populating `erc--msg-props'. +These override any defaults normally shown to modification hooks +by `erc-display-msg' and `erc-display-message'. Modules should +accommodate existing overrides when applicable. Items toward the +front shadow any that follow. Ignored when `erc--msg-props' is +already non-nil.") -(defvar tabbar--local-hlf) -(defvar motif-version-string) -(defvar gtk-version-string) +;; Forward declarations +(declare-function decoded-time-period "time-date" (time)) +(declare-function iso8601-parse-duration "iso8601" (string)) +(declare-function word-at-point "thingatpt" (&optional no-properties)) +(autoload 'word-at-point "thingatpt") ; for hl-nicks + +(declare-function gnutls-negotiate "gnutls" (&rest rest)) +(declare-function socks-open-network-stream "socks" (name buffer host service)) +(declare-function url-host "url-parse" (cl-x)) +(declare-function url-password "url-parse" (cl-x)) +(declare-function url-portspec "url-parse" (cl-x)) +(declare-function url-type "url-parse" (cl-x)) +(declare-function url-user "url-parse" (cl-x)) ;; tunable connection and authentication parameters @@ -237,7 +309,14 @@ node `(auth) Top' and Info node `(erc) auth-source'.") :type 'boolean) (defcustom erc-warn-about-blank-lines t - "Warn the user if they attempt to send a blank line." + "Warn the user if they attempt to send a blank line. +When non-nil, ERC signals a `user-error' upon encountering prompt +input containing empty or whitespace-only lines. When nil, ERC +still inhibits sending but does so silently. With the companion +option `erc-send-whitespace-lines' enabled, ERC sends pending +input and prints a message in the echo area indicating the amount +of padding and/or stripping applied, if any. Setting this option +to nil suppresses such reporting." :group 'erc :type 'boolean) @@ -249,8 +328,8 @@ node `(auth) Top' and Info node `(erc) auth-source'.") (defcustom erc-inhibit-multiline-input nil "When non-nil, conditionally disallow input consisting of multiple lines. Issue an error when the number of input lines submitted for -sending exceeds this value. The value t means disallow more -than 1 line of input." +sending meets or exceeds this value. The value t is synonymous +with a value of 2 and means disallow more than 1 line of input." :package-version '(ERC . "5.5") :group 'erc :type '(choice integer boolean)) @@ -271,8 +350,13 @@ than 1 line of input." "If non-nil, hide input prompt upon disconnecting. To unhide, type something in the input area. Once revealed, a prompt remains unhidden until the next disconnection. Channel -prompts are unhidden upon rejoining. See -`erc-unhide-query-prompt' for behavior concerning query prompts." +prompts are unhidden upon rejoining. For behavior concerning +query prompts, see `erc-unhide-query-prompt'. Longtime ERC users +should note that this option was repurposed in ERC 5.5 because it +had lain dormant for years after being sidelined in 5.3 when its +only use in the interactive client was removed. Before then, its +role was controlling whether `erc-command-indicator' would appear +alongside echoed slash-command lines." :package-version '(ERC . "5.5") :group 'erc-display :type '(choice (const :tag "Always hide prompt" t) @@ -309,6 +393,16 @@ If nil, only \"> \" will be shown." (const "PART") (const "QUIT") (const "MODE") + (const :tag "Away notices (RPL_AWAY 301)" "301") + (const :tag "Self back notice (REP_UNAWAY 305)" "305") + (const :tag "Self away notice (REP_NOWAWAY 306)" "306") + (const :tag "Channel modes on join (RPL_CHANNELMODEIS 324)" "324") + (const :tag "Channel creation time (RPL_CREATIONTIME 329)" "329") + (const :tag "Channel no-topic on join (RPL_NOTOPIC 331)" "331") + (const :tag "Channel topic on join (RPL_TOPIC 332)" "332") + (const :tag "Topic author and time on join (RPL_TOPICWHOTIME 333)" "333") + (const :tag "Invitation success notice (RPL_INVITING 341)" "341") + (const :tag "Channel member names (353 RPL_NAMEREPLY)" "353") (repeat :inline t :tag "Others" (string :tag "IRC Message Type")))) (defcustom erc-hide-list nil @@ -344,7 +438,7 @@ simply because we do not necessarily receive the QUIT event." :type 'hook) (defcustom erc-complete-functions nil - "These functions get called when the user hits TAB in ERC. + "These functions get called when the user hits \\`TAB' in ERC. Each function in turn is called until one returns non-nil to indicate it has handled the input." :group 'erc-hooks @@ -397,13 +491,14 @@ Functions are passed a buffer as the first argument." :group 'erc-hooks :type 'hook) - -(defvar-local erc-channel-users nil +(defvaralias 'erc-channel-users 'erc-channel-members) +(defvar-local erc-channel-members nil "Hash table of members in the current channel. -It associates nicknames with cons cells of the form: -\(USER . MEMBER-DATA) where USER is a pointer to an -erc-server-user struct, and MEMBER-DATA is a pointer to an -erc-channel-user struct.") +It associates nicknames with cons cells of the form +\(SERVER-USER . MEMBER-DATA), where SERVER-USER is a +`erc-server-user' object and MEMBER-DATA is a `erc-channel-user' +object. Convenient abbreviations for these two components are +`susr' and `cusr', along with `cmem' for the pair.") (defvar-local erc-server-users nil "Hash table of users on the current server. @@ -510,6 +605,8 @@ See also: `erc-remove-server-user' and Removes all users in the current channel. This is called by `erc-server-PART' and `erc-server-QUIT'." + (when (erc--target-channel-p erc--target) + (setf (erc--target-channel-joined-p erc--target) nil)) (when (and erc-server-connected (erc-server-process-alive) (hash-table-p erc-channel-users)) @@ -518,6 +615,53 @@ Removes all users in the current channel. This is called by erc-channel-users) (clrhash erc-channel-users))) +(defmacro erc--define-channel-user-status-compat-getter (name c d) + "Define a gv getter for historical `erc-channel-user' status slot NAME. +Expect NAME to be a string, C to be its traditionally associated +letter, and D to be its fallback power-of-2 integer for non-ERC +buffers." + `(defun ,(intern (concat "erc-channel-user-" name)) (u) + ,(format "Get equivalent of pre-5.6 `%s' slot for `erc-channel-user'." + name) + (declare (gv-setter (lambda (v) + (macroexp-let2 nil v v + (,'\`(let ((val (erc-channel-user-status ,',u)) + (n (or (erc--get-prefix-flag ,c) ,d))) + (setf (erc-channel-user-status ,',u) + (if ,',v + (logior val n) + (logand val (lognot n)))) + ,',v)))))) + (let ((n (or (erc--get-prefix-flag ,c) ,d))) + (= n (logand n (erc-channel-user-status u)))))) + +(erc--define-channel-user-status-compat-getter "voice" ?v 1) +(erc--define-channel-user-status-compat-getter "halfop" ?h 2) +(erc--define-channel-user-status-compat-getter "op" ?o 4) +(erc--define-channel-user-status-compat-getter "admin" ?a 8) +(erc--define-channel-user-status-compat-getter "owner" ?q 16) + +;; This is a generalized version of the compat-oriented getters above. +(defun erc--cusr-status-p (nick-or-cusr letter) + "Return non-nil if NICK-OR-CUSR has channel membership status LETTER." + (and-let* ((cusr (or (and (erc-channel-user-p nick-or-cusr) nick-or-cusr) + (cdr (erc-get-channel-member nick-or-cusr)))) + (n (erc--get-prefix-flag letter))) + (= n (logand n (erc-channel-user-status cusr))))) + +(defun erc--cusr-change-status (nick-or-cusr letter enablep &optional resetp) + "Add or remove membership status associated with LETTER for NICK-OR-CUSR. +With RESETP, clear the user's status info completely. If ENABLEP +is non-nil, add the status value associated with LETTER." + (when-let ((cusr (or (and (erc-channel-user-p nick-or-cusr) nick-or-cusr) + (cdr (erc-get-channel-member nick-or-cusr)))) + (n (erc--get-prefix-flag letter))) + (cl-callf (lambda (v) + (if resetp + (if enablep n 0) + (if enablep (logior v n) (logand v (lognot n))))) + (erc-channel-user-status cusr)))) + (defun erc-channel-user-owner-p (nick) "Return non-nil if NICK is an owner of the current channel." (and nick @@ -650,9 +794,9 @@ See also: `erc-get-channel-user-list'." "A topic string for the channel. Should only be used in channel-buffers.") (defvar-local erc-channel-modes nil - "List of strings representing channel modes. -E.g. (\"i\" \"m\" \"s\" \"b Quake!*@*\") -\(not sure the ban list will be here, but why not)") + "List of letters, as strings, representing channel modes. +For example, (\"i\" \"m\" \"s\"). Modes that take accompanying +parameters are not included.") (defvar-local erc-insert-marker nil "The place where insertion of new text in erc buffers should happen.") @@ -669,7 +813,74 @@ E.g. (\"i\" \"m\" \"s\" \"b Quake!*@*\") (defcustom erc-prompt "ERC>" "Prompt used by ERC. Trailing whitespace is not required." :group 'erc-display - :type '(choice string function)) + :type '(choice string + (function-item :tag "Interpret format specifiers" + erc-prompt-format) + function)) + +(defvar erc--prompt-format-face-example + #("%p%m%a\u00b7%b>" + 0 2 (font-lock-face erc-my-nick-prefix-face) + 2 4 (font-lock-face font-lock-keyword-face) + 4 6 (font-lock-face erc-error-face) + 6 7 (font-lock-face shadow) + 7 9 (font-lock-face font-lock-constant-face) + 9 10 (font-lock-face shadow)) + "An example value for option `erc-prompt-format' with faces.") + +(defcustom erc-prompt-format erc--prompt-format-face-example + "Format string when `erc-prompt' is `erc-prompt-format'. +ERC recognizes these substitution specifiers: + + %a - away indicator + %b - buffer name + %t - channel or query target, server domain, or dialed address + %S - target@network or buffer name + %s - target@server or server + %N - current network, like Libera.Chat + %p - channel membership prefix, like @ or + + %n - current nickname + %c - channel modes with args for select modes + %C - channel modes with all args + %u - user modes + %m - channel modes sans args in channels, user modes elsewhere + %M - like %m but show nothing in query buffers + +To pick your own colors, do something like: + + (setopt erc-prompt-format + (concat + (propertize \"%b\" \\='font-lock-face \\='erc-input-face) + (propertize \"%a\" \\='font-lock-face \\='erc-error-face))) + +Please remember that ERC ignores this option completely unless +the \"parent\" option `erc-prompt' is set to `erc-prompt-format'." + :package-version '(ERC . "5.6") + :group 'erc-display + :type `(choice (const :tag "{Prefix}{Mode}{Away}{MIDDLE DOT}{Buffer}>" + ,erc--prompt-format-face-example) + string)) + +(defun erc-prompt-format () + "Make predefined `format-spec' substitutions. + +See option `erc-prompt-format' and option `erc-prompt'." + (format-spec erc-prompt-format + (erc-compat--defer-format-spec-in-buffer + (?C erc--channel-modes 3 ",") + (?M erc--format-modes 'no-query-p) + (?N erc-format-network) + (?S erc-format-target-and/or-network) + (?a erc--format-away-indicator) + (?b buffer-name) + (?c erc-format-channel-modes) + (?m erc--format-modes) + (?n erc-current-nick) + (?p erc--format-channel-status-prefix) + (?s erc-format-target-and/or-server) + (?t erc-format-target) + (?u erc--format-user-modes)) + 'ignore-missing)) ; formerly `only-present' (defun erc-prompt () "Return the input prompt as a string. @@ -682,28 +893,6 @@ See also the variable `erc-prompt'." (concat prompt " ") prompt))) -(defcustom erc-command-indicator nil - "Indicator used by ERC for showing commands. - -If non-nil, this will be used in the ERC buffer to indicate -commands (i.e., input starting with a `/'). - -If nil, the prompt will be constructed from the variable `erc-prompt'." - :group 'erc-display - :type '(choice (const nil) string function)) - -(defun erc-command-indicator () - "Return the command indicator prompt as a string. - -This only has any meaning if the variable `erc-command-indicator' is non-nil." - (and erc-command-indicator - (let ((prompt (if (functionp erc-command-indicator) - (funcall erc-command-indicator) - erc-command-indicator))) - (if (> (length prompt) 0) - (concat prompt " ") - prompt)))) - (defcustom erc-notice-prefix "*** " "Prefix for all notices." :group 'erc-display @@ -881,6 +1070,9 @@ Flooding is sending too much information to the server in too short of an interval, which may cause the server to terminate the connection. +Note that older code conflated rate limiting and line splitting. +Starting in ERC 5.6, this option no longer influences the latter. + See `erc-server-flood-margin' for other flood-related parameters.") ;; Script parameters @@ -1008,7 +1200,13 @@ user after \"/PART\"." ;; Hooks (defgroup erc-hooks nil - "Hook variables for fancy customizations of ERC." + "Hooks for ERC. +Users of the interactive client should be aware that many of +these hooks have names predating the modern convention of +conveying abnormality via the \"-function\" suffix. Users should +likewise be aware that built-in and third-party modules use these +hooks as well, and some of their variables may be buffer-local in +particular sessions and/or `let'-bound for spells." :group 'erc) (defcustom erc-mode-hook nil @@ -1018,9 +1216,8 @@ user after \"/PART\"." :options '(erc-add-scroll-to-bottom)) (defcustom erc-timer-hook nil - "Put functions which should get called more or less periodically here. -The idea is that servers always play ping pong with the client, and so there -is no need for any idle-timer games with Emacs." + "Abnormal hook run after each response handler. +Called with a float returned from `erc-current-time'." :group 'erc-hooks :type 'hook) @@ -1055,41 +1252,54 @@ anyway." (make-obsolete-variable 'erc-send-pre-hook 'erc-pre-send-functions "27.1") (defcustom erc-pre-send-functions nil - "Special hook run to possibly alter the string that is sent. -The functions are called with one argument, an `erc-input' struct, -and should alter that struct. - -The struct has three slots: + "Special hook to possibly alter the string to send and insert. +ERC calls the member functions with one argument, an `erc-input' +struct instance to modify as needed. - `string': The current input string. - `insertp': Whether the string should be inserted into the erc buffer. - `sendp': Whether the string should be sent to the irc server." - :group 'erc - :type 'hook - :version "27.1") +The struct has five slots: -;; This is being auditioned for possible exporting (as a custom hook -;; option). Likewise for (public versions of) `erc--input-split' and -;; `erc--discard-trailing-multiline-nulls'. If unneeded, we'll just -;; run the latter on the input after `erc-pre-send-functions', and -;; remove this hook and the struct completely. IOW, if you need this, -;; please say so. + `string': String to send, originally from prompt input. + `insertp': Whether a string should be inserted in the buffer. + `sendp': Whether `string' should be sent to the IRC server. + `substxt': String to display (but not send) instead of `string'. + `refoldp': Whether to re-split `string' per protocol limits. + +This hook runs after protocol line splitting has taken place, so +the value of `string' comes \"pre-split\" according to the option +`erc-split-line-length'. If you need ERC to refill the entire +payload before sending it, set the `refoldp' slot to a non-nil +value. Note that this refilling is only a convenience, and +modules with special needs, such as preserving \"preformatted\" +text or encoding for subprotocol \"tunneling\", should handle +splitting manually and possibly also specify replacement text to +display via the `substxt' slot." + :package-version '(ERC . "5.3") + :group 'erc-hooks + :type 'hook) -(defvar erc--pre-send-split-functions '(erc--discard-trailing-multiline-nulls) - "Special hook for modifying individual lines in multiline prompt input. -The functions are called with one argument, an `erc--input-split' -struct, which they can optionally modify. +(define-obsolete-variable-alias 'erc--pre-send-split-functions + 'erc--input-review-functions "30.1") +(defvar erc--input-review-functions '(erc--split-lines + erc--run-input-validation-checks + erc--discard-trailing-multiline-nulls + erc--inhibit-slash-cmd-insertion) + "Special hook for reviewing and modifying prompt input. +ERC runs this before clearing the prompt and before running any +send-related hooks, such as `erc-pre-send-functions'. Thus, it's +quite \"safe\" to bail out of this hook with a `user-error', if +necessary. The hook's members are called with one argument, an +`erc--input-split' struct, which they can optionally modify. The struct has five slots: - `string': the input string delivered by `erc-pre-send-functions' - `insertp': whether to insert the lines into the buffer - `sendp': whether the lines should be sent to the IRC server + `string': the original input as a read-only reference + `insertp': same as in `erc-pre-send-functions' + `sendp': same as in `erc-pre-send-functions' + `refoldp': same as in `erc-pre-send-functions' `lines': a list of lines to be sent, each one a `string' `cmdp': whether to interpret input as a command, like /ignore -The `string' field is effectively read-only. When `cmdp' is -non-nil, all but the first line will be discarded.") +When `cmdp' is non-nil, all but the first line will be discarded.") (defvar erc-insert-this t "Insert the text into the target buffer or not. @@ -1106,9 +1316,13 @@ if they wish to avoid sending of a particular string.") "Insertion hook for functions that will change the text's appearance. This hook is called just after `erc-insert-pre-hook' when the value of `erc-insert-this' is t. -While this hook is run, narrowing is in effect and `current-buffer' is -the buffer where the text got inserted. One possible value to add here -is `erc-fill'." + +ERC runs this hook with the buffer narrowed to the bounds of the +inserted message plus a trailing newline. Built-in modules place +their hook members in two depth ranges: the first between -80 and +-20 and the second between 20 and 80. Use the functions +`erc-find-parsed-property' and `erc-get-parsed-vector' to locate +and extract the `erc-response' object for the inserted message." :group 'erc-hooks :type 'hook) @@ -1131,8 +1345,8 @@ preserve point if needed." (defcustom erc-send-modify-hook nil "Sending hook for functions that will change the text's appearance. -This hook is called just after `erc-send-pre-hook' when the values -of `erc-send-this' and `erc-insert-this' are both t. +ERC runs this just after `erc-pre-send-functions' if its shared +`erc-input' object's `sendp' and `insertp' slots remain non-nil. While this hook is run, narrowing is in effect and `current-buffer' is the buffer where the text got inserted. @@ -1189,7 +1403,6 @@ which the local user typed." (define-key map [home] #'erc-bol) (define-key map "\C-c\C-a" #'erc-bol) (define-key map "\C-c\C-b" #'erc-switch-to-buffer) - (define-key map "\C-c\C-c" #'erc-toggle-interpret-controls) (define-key map "\C-c\C-d" #'erc-input-action) (define-key map "\C-c\C-e" #'erc-toggle-ctcp-autoresponse) (define-key map "\C-c\C-f" #'erc-toggle-flood-control) @@ -1204,7 +1417,7 @@ which the local user typed." (define-key map "\C-c\C-u" #'erc-kill-input) (define-key map "\C-c\C-x" #'erc-quit-server) (define-key map "\M-\t" #'ispell-complete-word) - (define-key map "\t" #'completion-at-point) + (define-key map "\t" #'erc-tab) ;; Suppress `font-lock-fontify-block' key binding since it ;; destroys face properties. @@ -1213,6 +1426,19 @@ which the local user typed." map) "ERC keymap.") +(defun erc--modify-local-map (mode &rest bindings) + "Modify `erc-mode-map' on behalf of a global module. +Add or remove `key-valid-p' BINDINGS when toggling MODE." + (declare (indent 1)) + (while (pcase-let* ((`(,key ,def . ,rest) bindings) + (existing (keymap-lookup erc-mode-map key))) + (if mode + (when (or (not existing) (eq existing #'undefined)) + (keymap-set erc-mode-map key def)) + (when (eq existing def) + (keymap-unset erc-mode-map key t))) + (setq bindings rest)))) + ;; Faces ; Honestly, I have a horrible sense of color and the "defaults" below @@ -1260,21 +1486,20 @@ This will only be used if `erc-header-line-face-method' is non-nil." "ERC face for the prompt." :group 'erc-faces) -(defface erc-command-indicator-face - '((t :weight bold)) - "ERC face for the command indicator. -See the variable `erc-command-indicator'." - :group 'erc-faces) - (defface erc-notice-face '((default :weight bold) + (((class color) (min-colors 88) (supports :weight semi-bold)) + :weight semi-bold :foreground "SlateBlue") (((class color) (min-colors 88)) :foreground "SlateBlue") (t :foreground "blue")) "ERC face for notices." + :package-version '(ERC . "5.6") :group 'erc-faces) -(defface erc-action-face '((t :weight bold)) +(defface erc-action-face '((((supports :weight semi-bold)) :weight semi-bold) + (t :weight bold)) "ERC face for actions generated by /ME." + :package-version '(ERC . "5.6") :group 'erc-faces) (defface erc-error-face '((t :foreground "red")) @@ -1297,9 +1522,8 @@ See also `erc-show-my-nick'." ;; Debugging support -(defvar erc-log-p nil - "When set to t, generate debug messages in a separate debug buffer.") - +;; FIXME if this variable plays some role, indicate that here. +;; Otherwise, deprecate. (defvar erc-debug-log-file (expand-file-name "ERC.debug") "Debug log file name.") @@ -1313,6 +1537,21 @@ See also `erc-show-my-nick'." Bound to local variables from an existing (logical) session's buffer during local-module setup and `erc-mode-hook' activation.") +(defmacro erc--restore-initialize-priors (mode &rest vars) + "Restore local VARS for local minor MODE from a previous session." + (declare (indent 1)) + (let ((priors (make-symbol "priors")) + (initp (make-symbol "initp")) + ;; + forms) + (while-let ((k (pop vars))) + (push `(,k (if ,initp (alist-get ',k ,priors) ,(pop vars))) forms)) + `(let* ((,priors (or erc--server-reconnecting erc--target-priors)) + (,initp (and ,priors (alist-get ',mode ,priors)))) + (unless (local-variable-if-set-p ',mode) + (error "Not a local minor mode var: %s" ',mode)) + (setq ,@(mapcan #'identity (nreverse forms)))))) + (defun erc--target-from-string (string) "Construct an `erc--target' variant from STRING." (funcall (if (erc-channel-p string) @@ -1322,16 +1561,6 @@ buffer during local-module setup and `erc-mode-hook' activation.") #'make-erc--target) :string string :symbol (intern (erc-downcase string)))) -(defvar-local erc--target nil - "Info about a buffer's target, if any.") - -;; Temporary internal getter to ease transition to `erc--target' -;; everywhere. Will be replaced by updated `erc-default-target'. -(defun erc--default-target () - "Return target string or nil." - (when erc--target - (erc--target-string erc--target))) - (defun erc-once-with-server-event (event f) "Run function F the next time EVENT occurs in the `current-buffer'. @@ -1348,7 +1577,7 @@ Please be sure to use this function in server-buffers. In channel-buffers it may not work at all, as it uses the LOCAL argument of `add-hook' and `remove-hook' to ensure multiserver capabilities." - (unless (erc-server-buffer-p) + (unless (erc--server-buffer-p) (error "You should only run `erc-once-with-server-event' in a server buffer")) (let ((fun (make-symbol "fun")) @@ -1362,6 +1591,37 @@ capabilities." (add-hook hook fun nil t) fun)) +(defun erc--warn-once-before-connect (mode-var &rest args) + "Display an \"error notice\" once. +Expect ARGS to be `erc-button--display-error-notice-with-keys' +compatible parameters, except without any leading buffers or +processes. If we're in an ERC buffer with a network process when +called, print the notice immediately. Otherwise, if we're in a +server buffer, arrange to do so after local modules have been set +up and mode hooks have run. Otherwise, if MODE-VAR is a global +module, try again at most once the next time `erc-mode-hook' +runs." + (declare (indent 1)) + (cl-assert (stringp (car args))) + (if (derived-mode-p 'erc-mode) + (unless (or (erc-with-server-buffer ; needs `erc-server-process' + (apply #'erc-button--display-error-notice-with-keys + (current-buffer) args) + t) + erc--target) ; unlikely + (let (hook) + (setq hook + (lambda (_) + (remove-hook 'erc-connect-pre-hook hook t) + (apply #'erc-button--display-error-notice-with-keys args))) + (add-hook 'erc-connect-pre-hook hook nil t))) + (when (custom-variable-p mode-var) + (let (hook) + (setq hook (lambda () + (remove-hook 'erc-mode-hook hook) + (apply #'erc--warn-once-before-connect 'erc-fake args))) + (add-hook 'erc-mode-hook hook))))) + (defun erc-server-buffer () "Return the server buffer for the current buffer's process. The buffer-local variable `erc-server-process' is used to find @@ -1374,29 +1634,36 @@ the process buffer." (and (processp erc-server-process) (buffer-live-p (process-buffer erc-server-process)))) -(defun erc-server-buffer-p (&optional buffer) +(define-obsolete-function-alias + 'erc-server-buffer-p 'erc-server-or-unjoined-channel-buffer-p "30.1") +(defun erc-server-or-unjoined-channel-buffer-p (&optional buffer) "Return non-nil if argument BUFFER is an ERC server buffer. - -If BUFFER is nil, the current buffer is used." +If BUFFER is nil, use the current buffer. For historical +reasons, also return non-nil for channel buffers the client has +parted or from which it's been kicked." (with-current-buffer (or buffer (current-buffer)) (and (eq major-mode 'erc-mode) (null (erc-default-target))))) +(defun erc--server-buffer-p (&optional buffer) + "Return non-nil if BUFFER is an ERC server buffer. +Without BUFFER, use the current buffer." + (if buffer + (with-current-buffer buffer + (and (eq major-mode 'erc-mode) (null erc--target))) + (and (eq major-mode 'erc-mode) (null erc--target)))) + (defun erc-open-server-buffer-p (&optional buffer) "Return non-nil if BUFFER is an ERC server buffer with an open IRC process. If BUFFER is nil, the current buffer is used." - (and (erc-server-buffer-p buffer) + (and (erc--server-buffer-p buffer) (erc-server-process-alive buffer))) (defun erc-query-buffer-p (&optional buffer) "Return non-nil if BUFFER is an ERC query buffer. If BUFFER is nil, the current buffer is used." - (with-current-buffer (or buffer (current-buffer)) - (let ((target (erc-default-target))) - (and (eq major-mode 'erc-mode) - target - (not (memq (aref target 0) '(?# ?& ?+ ?!))))))) + (not (erc-channel-p (or buffer (current-buffer))))) (defun erc-ison-p (nick) "Return non-nil if NICK is online." @@ -1454,6 +1721,7 @@ Defaults to the server buffer." (setq-local paragraph-start (concat "\\(" (regexp-quote (erc-prompt)) "\\)")) (setq-local completion-ignore-case t) + (add-hook 'post-command-hook #'erc-check-text-conversion nil t) (add-hook 'kill-buffer-hook #'erc-kill-buffer-function nil t) (add-hook 'completion-at-point-functions #'erc-complete-word-at-point nil t)) @@ -1462,15 +1730,33 @@ Defaults to the server buffer." (defconst erc-default-server "irc.libera.chat" "IRC server to use if it cannot be detected otherwise.") -(defconst erc-default-port 6667 +(defvar erc-default-port 6667 "IRC port to use if it cannot be detected otherwise.") (defconst erc-default-port-tls 6697 "IRC port to use for encrypted connections if it cannot be \ detected otherwise.") -(defcustom erc-join-buffer 'bury - "Determines how to display a newly created IRC buffer. +(defconst erc--buffer-display-choices + `(choice (const :tag "Use value of `erc-buffer-display'" nil) + (const :tag "Split window and select" window) + (const :tag "Split window but don't select" window-noselect) + (const :tag "New frame" frame) + (const :tag "Don't display" bury) + (const :tag "Use current window" buffer) + (choice :tag "Defer to a display function" + (function-item display-buffer) + (function-item pop-to-buffer) + (function :tag "User-defined"))) + "Common choices for buffer-display options.") + +(defvaralias 'erc-join-buffer 'erc-buffer-display) +(defcustom erc-buffer-display 'bury + "How to display a newly created ERC buffer. +This determines ERC's baseline, \"catch-all\" buffer-display +behavior. It takes a backseat to more specific options, like +`erc-interactive-display', `erc-auto-reconnect-display', and +`erc-receive-query-display'. The available choices are: @@ -1479,41 +1765,93 @@ The available choices are: `frame' - in another frame, `bury' - bury it in a new buffer, `buffer' - in place of the current buffer, - any other value - in place of the current buffer." + DISPLAY-FUNCTION - a `display-buffer'-like function + +Here, DISPLAY-FUNCTION should accept a buffer and an ACTION of +the kind described by the Info node `(elisp) Choosing Window'. +At times, ERC may add hints about the calling context to the +ACTION's alist. Keys are symbols such as user options, like +`erc-buffer-display', or module minor modes, like +`erc-autojoin-mode'. Values are non-nil constants specific to +each. For this particular option, possible values include the +symbols + + `JOIN', `PRIVMSG', `NOTICE', `erc', and `erc-tls'. + +The first three signify IRC commands received from the server and +the rest entry-point commands responsible for the connection. +When dealing with the latter two, users may prefer to set this +option to `bury' and instead call DISPLAY-FUNCTION directly +on (server) buffers returned by these entry points because the +context leading to their creation is plainly obvious. For +additional details, see the Info node `(erc) display-buffer'. + +Note that when the selected window already shows the current +buffer, ERC pretends this option's value is `bury' unless the +variable `erc-skip-displaying-selected-window-buffer' is nil or +the value of this option is DISPLAY-FUNCTION." :package-version '(ERC . "5.5") :group 'erc-buffers - :type '(choice (const :tag "Split window and select" window) - (const :tag "Split window, don't select" window-noselect) - (const :tag "New frame" frame) - (const :tag "Bury in new buffer" bury) - (const :tag "Use current buffer" buffer) - (const :tag "Use current buffer" t))) - -(defcustom erc-reconnect-display nil - "How (and whether) to display a channel buffer upon reconnecting. - -This only affects automatic reconnections and is ignored when -issuing a /reconnect command or reinvoking `erc-tls' with the -same args (assuming success, of course). See `erc-join-buffer' -for a description of possible values. - -WARNING: this option is bugged in ERC 5.5 (Emacs 29). Setting it -to anything other than nil results in the chosen value being -permanently adopted by all other buffer-display options for the -remainder of the ERC session. If you need this fixed -immediately, see Info node `(erc) Upgrading'." + :type (cons 'choice (nthcdr 2 erc--buffer-display-choices))) + +(defvaralias 'erc-query-display 'erc-interactive-display) +(defcustom erc-interactive-display 'window + "How to display buffers as a result of user interaction. +This affects commands like /QUERY and /JOIN when issued +interactively at the prompt. It does not apply when calling a +handler for such a command, like `erc-cmd-JOIN', from lisp code. +See `erc-buffer-display' for a full description of available +values. + +When the value is a user-provided function, ERC may inject a hint +about the invocation context as an extra item in the \"action +alist\" included as part of the second argument. The item's key +is the symbol `erc-interactive-display' and its value one of + + `/QUERY', `/JOIN', `/RECONNECT', `url', `erc', or `erc-tls'. + +All are symbols indicating an inciting user action, such as the +issuance of a slash command, the clicking of a URL hyperlink, or +the invocation of an entry-point command. See Info node `(erc) +display-buffer' for more." + :package-version '(ERC . "5.6") + :group 'erc-buffers + :type erc--buffer-display-choices) + +(defvaralias 'erc-reconnect-display 'erc-auto-reconnect-display) +(defcustom erc-auto-reconnect-display nil + "How to display a channel buffer when automatically reconnecting. +ERC ignores this option when a user issues a /RECONNECT or +successfully reinvokes `erc-tls' with similar arguments to those +from the prior connection. See `erc-buffer-display' for a +description of possible values. + +When the value is function, ERC may inject a hint about the +calling context as an extra item in the alist making up the tail +of the second, \"action\" argument. The item's key is the symbol +`erc-auto-reconnect-display' and its value something non-nil." :package-version '(ERC . "5.5") :group 'erc-buffers - :set (lambda (sym val) - (when (set sym val) - (lwarn 'erc :warning "Setting `%s' to `%s' is currently bugged; %s" - sym val "see doc string for more information."))) - :type '(choice (const :tag "Use value of `erc-join-buffer'" nil) - (const :tag "Split window and select" window) - (const :tag "Split window, don't select" window-noselect) - (const :tag "New frame" frame) - (const :tag "Bury in new buffer" bury) - (const :tag "Use current buffer" buffer))) + :type erc--buffer-display-choices) + +(defcustom erc-auto-reconnect-display-timeout 10 + "Duration `erc-auto-reconnect-display' remains active. +The countdown starts on MOTD and is canceled early by any +\"slash\" command." + :package-version '(ERC . "5.6") + :type 'integer + :group 'erc-buffers) + +(defcustom erc-reconnect-display-server-buffers nil + "Apply buffer-display options to server buffers when reconnecting. +By default, ERC does not consider `erc-auto-reconnect-display' +for server buffers when automatically reconnecting, nor does it +consider `erc-interactive-display' when users issue a /RECONNECT. +Enabling this tells ERC to always display server buffers +according to those options." + :package-version '(ERC . "5.6") + :type 'boolean + :group 'erc-buffers) (defcustom erc-frame-alist nil "Alist of frame parameters for creating erc frames. @@ -1531,20 +1869,29 @@ This only has effect when `erc-join-buffer' is set to `frame'." (defcustom erc-reuse-frames t "Determines whether new frames are always created. -Non-nil means that a new frame is not created to display an ERC -buffer if there is already a window displaying it. This only has -effect when `erc-join-buffer' is set to `frame'." +Non-nil means only create a frame for undisplayed buffers. Nil +means always create a new frame. Regardless of its value, ERC +ignores this option unless `erc-join-buffer' is `frame'. And +like most options in the `erc-buffer' customize group, this has +no effect on server buffers while reconnecting because ERC always +buries those." :group 'erc-buffers :type 'boolean) -(defun erc-channel-p (channel) - "Return non-nil if CHANNEL seems to be an IRC channel name." - (cond ((stringp channel) - (memq (aref channel 0) '(?# ?& ?+ ?!))) - ((and (bufferp channel) (buffer-live-p channel)) - (with-current-buffer channel - (erc-channel-p (erc-default-target)))) - (t nil))) +(defvar erc--fallback-channel-prefixes "#&" + "Prefix chars for distinguishing channel targets when CHANTYPES is unknown.") + +(defun erc-channel-p (target) + "Return non-nil if TARGET is a valid channel name or a channel buffer." + (cond ((stringp target) + (and-let* + (((not (string-empty-p target))) + (value (let ((entry (erc--get-isupport-entry 'CHANTYPES))) + (if entry (cadr entry) erc--fallback-channel-prefixes))) + ((erc--strpos (aref target 0) value))))) + ((and-let* (((buffer-live-p target)) + (target (buffer-local-value 'erc--target target)) + ((erc--target-channel-p target))))))) ;; For the sake of compatibility, a historical quirk concerning this ;; option, when nil, has been preserved: all buffers are suffixed with @@ -1680,7 +2027,10 @@ All strings are compared according to IRC protocol case rules, see (defun erc-get-buffer (target &optional proc) "Return the buffer matching TARGET in the process PROC. -If PROC is not supplied, all processes are searched." +Without PROC, search all ERC buffers. For historical reasons, +skip buffers for channels the client has \"PART\"ed or from which +it's been \"KICK\"ed. Expect users to use a different function +for finding targets independent of \"JOIN\"edness." (let ((downcased-target (erc-downcase target))) (catch 'buffer (erc-buffer-filter @@ -1701,8 +2051,9 @@ If PROC is not supplied, all processes are searched." (defun erc-buffer-filter (predicate &optional proc) "Return a list of `erc-mode' buffers matching certain criteria. -PREDICATE is a function executed with each buffer, if it returns t, that buffer -is considered a valid match. +Call PREDICATE without arguments in all ERC buffers or only those +belonging to a non-nil PROC. Expect it to return non-nil in +buffers that should be included in the returned list. PROC is either an `erc-server-process', identifying a certain server connection, or nil which means all open connections." @@ -1714,15 +2065,20 @@ server connection, or nil which means all open connections." (erc--buffer-p buf predicate proc))) (buffer-list))))) +(defalias 'erc-buffer-do 'erc-buffer-filter + "Call FUNCTION in all ERC buffers or only those for PROC. +Expect to be preferred over `erc-buffer-filter' in cases where +the return value goes unused. + +\(fn FUNCTION &optional PROC)") + (defun erc-buffer-list (&optional predicate proc) "Return a list of ERC buffers. PREDICATE is a function which executes with every buffer satisfying the predicate. If PREDICATE is passed as nil, return a list of all ERC buffers. If PROC is given, the buffers local variable `erc-server-process' needs to match PROC." - (unless predicate - (setq predicate (lambda () t))) - (erc-buffer-filter predicate proc)) + (erc-buffer-filter (or predicate #'always) proc)) (define-obsolete-function-alias 'erc-iswitchb #'erc-switch-to-buffer "25.1") (defun erc--switch-to-buffer (&optional arg) @@ -1826,51 +2182,77 @@ buffer rather than a server buffer.") ;; each item is in the format '(old . new) (delete-dups (mapcar #'erc--normalize-module-symbol mods))) -(defcustom erc-modules '(netsplit fill button match track completion readonly - networks ring autojoin noncommands irccontrols - move-to-prompt stamp menu list) - "A list of modules which ERC should enable. -If you set the value of this without using `customize' remember to call -\(erc-update-modules) after you change it. When using `customize', modules -removed from the list will be disabled." +(defun erc--sort-modules (modules) + "Return a copy of MODULES, deduped and led by sorted built-ins." + (let (built-in third-party) + (dolist (mod modules) + (setq mod (erc--normalize-module-symbol mod)) + (cl-pushnew mod (if (get mod 'erc--module) built-in third-party))) + `(,@(sort built-in #'string-lessp) ,@(nreverse third-party)))) + +;;;###autoload(custom-autoload 'erc-modules "erc") + +(defcustom erc-modules '( autojoin button completion fill imenu irccontrols + list match menu move-to-prompt netsplit + networks readonly ring stamp track) + "Modules to enable while connecting. +When modifying this option in lisp code, use a Custom-friendly +facilitator, like `setopt', or call `erc-update-modules' +afterward. This ensures a consistent ordering and disables +removed modules. It also gives packages access to the hook +`erc-before-connect'." :get (lambda (sym) ;; replace outdated names with their newer equivalents (erc-migrate-modules (symbol-value sym))) - :initialize #'custom-initialize-default + ;; Expect every built-in module to have the symbol property + ;; `erc--module' set to its canonical symbol (often itself). + :initialize (lambda (symbol exp) + ;; Use `cdddr' because (set :greedy t . ,entries) + (dolist (entry (cdddr (get 'erc-modules 'custom-type))) + (when-let* (((eq (car entry) 'const)) + (s (cadddr entry))) ; (const :tag "..." ,s) + (put s 'erc--module s))) + (custom-initialize-reset symbol exp)) :set (lambda (sym val) ;; disable modules which have just been removed (when (and (boundp 'erc-modules) erc-modules val) (dolist (module erc-modules) - (unless (member module val) + (unless (memq module val) (let ((f (intern-soft (format "erc-%s-mode" module)))) (when (and (fboundp f) (boundp f)) (when (symbol-value f) (message "Disabling `erc-%s'" module) (funcall f 0)) + ;; Disable local module in all ERC buffers. (unless (or (custom-variable-p f) (not (fboundp 'erc-buffer-filter))) (erc-buffer-filter (lambda () (when (symbol-value f) (funcall f 0)) (kill-local-variable f))))))))) - (set sym val) - ;; this test is for the case where erc hasn't been loaded yet + ;; Calling `set-default-toplevel-value' complicates testing. + (set sym (erc--sort-modules val)) + ;; Don't initialize modules on load, even though the rare + ;; third-party module may need it. (when (fboundp 'erc-update-modules) - (erc-update-modules))) + (unless erc--inside-mode-toggle-p + (erc-update-modules)))) :type '(set :greedy t (const :tag "autoaway: Set away status automatically" autoaway) (const :tag "autojoin: Join channels automatically" autojoin) + (const :tag "bufbar: Show ERC buffers in a side window" bufbar) (const :tag "button: Buttonize URLs, nicknames, and other text" button) (const :tag "capab: Mark unidentified users on servers supporting CAPAB" capab-identify) + (const :tag "command-indicator: Echo command lines." command-indicator) (const :tag "completion: Complete nicknames and commands (programmable)" completion) - (const :tag "hecomplete: Complete nicknames and commands (obsolete, use \"completion\")" hecomplete) (const :tag "dcc: Provide Direct Client-to-Client support" dcc) (const :tag "fill: Wrap long lines" fill) (const :tag "identd: Launch an identd server on port 8113" identd) + (const :tag "imenu: A simple Imenu integration" imenu) (const :tag "irccontrols: Highlight or remove IRC control characters" irccontrols) (const :tag "keep-place: Leave point above un-viewed text" keep-place) @@ -1882,13 +2264,15 @@ removed from the list will be disabled." move-to-prompt) (const :tag "netsplit: Detect netsplits" netsplit) (const :tag "networks: Provide data about IRC networks" networks) - (const :tag "noncommands: Don't display non-IRC commands after evaluation" + (const :tag "nickbar: Show nicknames in a dynamic side window" nickbar) + (const :tag "nicks: Uniquely colorize nicknames in target buffers" nicks) + (const :tag "noncommands: Deprecated. See module `command-indicator'." noncommands) + (const :tag "notifications: Desktop alerts on PRIVMSG or mentions" + notifications) (const :tag "notify: Notify when the online status of certain users changes" notify) - (const :tag "notifications: Send notifications on PRIVMSG or nickname mentions" - notifications) (const :tag "page: Process CTCP PAGE requests from IRC" page) (const :tag "readonly: Make displayed lines read-only" readonly) (const :tag "replace: Replace text in messages" replace) @@ -1901,40 +2285,130 @@ removed from the list will be disabled." (const :tag "smiley: Convert smileys to pretty icons" smiley) (const :tag "sound: Play sounds when you receive CTCP SOUND requests" sound) - (const :tag "stamp: Add timestamps to messages" stamp) (const :tag "spelling: Check spelling" spelling) + (const :tag "stamp: Add timestamps to messages" stamp) (const :tag "track: Track channel activity in the mode-line" track) (const :tag "truncate: Truncate buffers to a certain size" truncate) (const :tag "unmorse: Translate morse code in messages" unmorse) (const :tag "xdcc: Act as an XDCC file-server" xdcc) (repeat :tag "Others" :inline t symbol)) + :package-version '(ERC . "5.6") :group 'erc) (defun erc-update-modules () "Enable minor mode for every module in `erc-modules'. Except ignore all local modules, which were introduced in ERC 5.5." - (erc--update-modules) + (erc--update-modules erc-modules) nil) -(defun erc--update-modules () +(defvar erc--aberrant-modules nil + "Modules suspected of being improperly loaded.") + +(defun erc--warn-about-aberrant-modules () + (when (and erc--aberrant-modules (not erc--target)) + (erc-button--display-error-notice-with-keys-and-warn + "The following modules likely engage in unfavorable loading practices: " + (mapconcat (lambda (s) (format "`%s'" s)) erc--aberrant-modules ", ") + ". Please contact ERC with \\[erc-bug] if you believe this to be untrue." + " See Info:\"(erc) Module Loading\" for more.") + (setq erc--aberrant-modules nil))) + +(defvar erc--requiring-module-mode-p nil + "Non-nil while doing (require \\='erc-mymod) for `mymod' in `erc-modules'. +Used for inhibiting potentially recursive `erc-update-modules' +invocations by third-party packages.") + +(defun erc--find-mode (sym) + (setq sym (erc--normalize-module-symbol sym)) + (if-let ((mode (intern-soft (concat "erc-" (symbol-name sym) "-mode"))) + ((and (fboundp mode) + (autoload-do-load (symbol-function mode) mode))) + ((or (get sym 'erc--module) + (symbol-file mode) + (ignore (cl-pushnew sym erc--aberrant-modules))))) + mode + (and (or (and erc--requiring-module-mode-p + ;; Also likely non-nil: (eq sym (car features)) + (cl-pushnew sym erc--aberrant-modules)) + (let ((erc--requiring-module-mode-p t)) + (require (or (get sym 'erc--feature) + (intern (concat "erc-" (symbol-name sym)))) + nil 'noerror)) + (memq sym erc--aberrant-modules)) + (or mode (setq mode (intern-soft (concat "erc-" (symbol-name sym) + "-mode")))) + (fboundp mode) + mode))) + +(defun erc--update-modules (modules) (let (local-modes) - (dolist (module erc-modules local-modes) - (require (or (alist-get module erc--modules-to-features) - (intern (concat "erc-" (symbol-name module)))) - nil 'noerror) ; some modules don't have a corresponding feature - (let ((mode (intern-soft (concat "erc-" (symbol-name module) "-mode")))) - (unless (and mode (fboundp mode)) - (error "`%s' is not a known ERC module" module)) - (if (custom-variable-p mode) - (funcall mode 1) - (push mode local-modes)))))) + (dolist (module modules local-modes) + (if-let ((mode (erc--find-mode module))) + (if (custom-variable-p mode) + (funcall mode 1) + (push mode local-modes)) + (error "`%s' is not a known ERC module" module))))) + +(defvar erc--updating-modules-p nil + "Non-nil when running `erc--update-modules' in `erc-open'. +This allows global modules with known or likely dependents (or +some other reason for activating after session initialization) to +conditionally run setup code traditionally reserved for +`erc-mode-hook' in the setup portion of their mode toggle. Note +that being \"global\", they'll likely want to do so in all ERC +buffers and ensure the code is idempotent. For example: + + (add-hook \\='erc-mode-hook #\\='erc-foo-setup-fn) + (unless erc--updating-modules-p + (erc-with-all-buffers-of-server nil + (lambda () some-condition-p) + (erc-foo-setup-fn))) + +This means that when a dependent module is initializing and +realizes it's missing some required module \"foo\", it can +confidently call (erc-foo-mode 1) without having to learn +anything about the dependency's implementation.") + +(defvar erc--setup-buffer-hook '(erc--warn-about-aberrant-modules) + "Internal hook for module setup involving windows and frames.") + +(defvar erc--display-context nil + "Extra action alist items passed to `display-buffer'. +Non-nil when a user specifies a custom display action for certain +buffer-display options, like `erc-auto-reconnect-display'. ERC +pairs the option's symbol with a context-dependent value and adds +the entry to the user-provided alist when calling `pop-to-buffer' +or `display-buffer'.") + +(defvar erc-skip-displaying-selected-window-buffer t + "Whether to forgo showing a buffer that's already being displayed. +But only in the selected window. This is intended as a crutch +for non-user third-party code that might be slow to adopt the +`display-buffer' function variant available to all buffer-display +options starting in ERC 5.6. Users with rare requirements, like +wanting to change the window buffer to something other than the +one being processed, should see the Info node `(erc) +display-buffer'.") +(make-obsolete 'erc-show-already-displayed-buffer + "non-nil behavior to be made permanent" "30.1") + +(defvar-local erc--display-buffer-overriding-action nil + "The value of `display-buffer-overriding-action' when non-nil. +Influences the displaying of new or reassociated ERC buffers. +Reserved for use by built-in modules.") (defun erc-setup-buffer (buffer) "Consults `erc-join-buffer' to find out how to display `BUFFER'." (pcase (if (zerop (erc-with-server-buffer erc--server-last-reconnect-count)) erc-join-buffer - (or erc-reconnect-display erc-join-buffer)) + (or erc-auto-reconnect-display erc-join-buffer)) + ((and (pred functionp) disp-fn (let context erc--display-context)) + (unless (zerop erc--server-last-reconnect-count) + (push '(erc-auto-reconnect-display . t) context)) + (funcall disp-fn buffer (cons nil context))) + ((guard (and erc-skip-displaying-selected-window-buffer + (eq (window-buffer) buffer)))) ('window (if (active-minibuffer-window) (display-buffer buffer) @@ -1977,30 +2451,55 @@ nil." (cons (nreverse (car out)) (nreverse (cdr out)))) (list new-modes))) +;; This function doubles as a convenient helper for use in unit tests. +;; Prior to 5.6, its contents lived in `erc-open'. + +(defun erc--initialize-markers (old-point continued-session) + "Ensure prompt and its bounding markers have been initialized." + ;; FIXME erase assertions after code review and additional testing. + (setq erc-insert-marker (make-marker) + erc-input-marker (make-marker)) + (if continued-session + (progn + ;; Trust existing markers. + (set-marker erc-insert-marker + (alist-get 'erc-insert-marker continued-session)) + (set-marker erc-input-marker + (alist-get 'erc-input-marker continued-session)) + (set-marker-insertion-type erc-insert-marker t) + (cl-assert (= (field-end erc-insert-marker) erc-input-marker)) + (goto-char old-point) + (let ((erc--hidden-prompt-overlay + (alist-get 'erc--hidden-prompt-overlay continued-session))) + (erc--unhide-prompt))) + (cl-assert (not (get-text-property (point) 'erc-prompt))) + ;; In the original version from `erc-open', the snippet that + ;; handled these newline insertions appeared twice close in + ;; proximity, which was probably unintended. Nevertheless, we + ;; preserve the double newlines here for historical reasons. + (insert "\n\n") + (set-marker erc-insert-marker (point)) + (erc-display-prompt) + (set-marker-insertion-type erc-insert-marker t) + (cl-assert (= (point) (point-max))))) + (defun erc-open (&optional server port nick full-name - connect passwd tgt-list channel process + connect passwd _tgt-list channel process client-certificate user id) - "Connect to SERVER on PORT as NICK with USER and FULL-NAME. - -If CONNECT is non-nil, connect to the server. Otherwise assume -already connected and just create a separate buffer for the new -target given by CHANNEL, meaning these parameters are mutually -exclusive. Note that CHANNEL may also be a query; its name has -been retained for historical reasons. - -Use PASSWD as user password on the server. If TGT-LIST is -non-nil, use it to initialize `erc-default-recipients'. - -CLIENT-CERTIFICATE, if non-nil, should either be a list where the -first element is the file name of the private key corresponding -to a client certificate and the second element is the file name -of the client certificate itself to use when connecting over TLS, -or t, which means that `auth-source' will be queried for the -private key and the certificate. - -When non-nil, ID should be a symbol for identifying the connection. - -Returns the buffer for the given server or channel." + "Return a new or reinitialized server or target buffer. +If CONNECT is non-nil, connect to SERVER and return its new or +reassociated buffer. Otherwise, assume PROCESS is non-nil and belongs +to an active session, and return a new or refurbished target buffer for +CHANNEL, which may also be a query target (the parameter name remains +for historical reasons). Pass SERVER, PORT, NICK, USER, FULL-NAME, and +PASSWD to `erc-determine-parameters' for preserving as session-local +variables. Do something similar for CLIENT-CERTIFICATE and ID, which +should be as described by `erc-tls'. + +Note that ERC ignores TGT-LIST and initializes `erc-default-recipients' +with CHANNEL as its only member. Note also that this function has the +side effect of setting the current buffer to the one it returns. Use +`with-current-buffer' or `save-excursion' to nullify this effect." (let* ((target (and channel (erc--target-from-string channel))) (buffer (erc-get-buffer-create server port nil target id)) (old-buffer (current-buffer)) @@ -2010,15 +2509,20 @@ Returns the buffer for the given server or channel." (old-recon-count erc-server-reconnect-count) (old-point nil) (delayed-modules nil) - (continued-session (and erc--server-reconnecting - (with-suppressed-warnings - ((obsolete erc-reuse-buffers)) - erc-reuse-buffers)))) + (continued-session (or erc--server-reconnecting + erc--target-priors + (and-let* (((not target)) + (m (buffer-local-value + 'erc-input-marker buffer)) + ((marker-position m))) + (buffer-local-variables buffer))))) (when connect (run-hook-with-args 'erc-before-connect server port nick)) (set-buffer buffer) (setq old-point (point)) (setq delayed-modules - (erc--merge-local-modes (erc--update-modules) + (erc--merge-local-modes (let ((erc--updating-modules-p t)) + (erc--update-modules + (erc--sort-modules erc-modules))) (or erc--server-reconnecting erc--target-priors))) @@ -2031,23 +2535,8 @@ Returns the buffer for the given server or channel." (buffer-local-value 'erc-server-announced-name old-buffer))) ;; connection parameters (setq erc-server-process process) - (setq erc-insert-marker (make-marker)) - (setq erc-input-marker (make-marker)) - ;; go to the end of the buffer and open a new line - ;; (the buffer may have existed) - (goto-char (point-max)) - (forward-line 0) - (when (or continued-session (get-text-property (point) 'erc-prompt)) - (setq continued-session t) - (set-marker erc-input-marker - (or (next-single-property-change (point) 'erc-prompt) - (point-max)))) - (unless continued-session - (goto-char (point-max)) - (insert "\n")) - (set-marker erc-insert-marker (point)) ;; stack of default recipients - (setq erc-default-recipients tgt-list) + (when channel (setq erc-default-recipients (list channel))) (when target (setq erc--target target erc-network (erc-network))) @@ -2091,21 +2580,8 @@ Returns the buffer for the given server or channel." (when erc-log-p (get-buffer-create (concat "*ERC-DEBUG: " server "*")))) + (erc--initialize-markers old-point continued-session) (erc-determine-parameters server port nick full-name user passwd) - - ;; FIXME consolidate this prompt-setup logic with the pass above. - - ;; set up prompt - (unless continued-session - (goto-char (point-max)) - (insert "\n")) - (if continued-session - (progn (goto-char old-point) - (erc--unhide-prompt)) - (set-marker erc-insert-marker (point)) - (erc-display-prompt) - (goto-char (point-max))) - (save-excursion (run-mode-hooks) (dolist (mod (car delayed-modules)) (funcall mod +1)) (dolist (var (cdr delayed-modules)) (set var nil))) @@ -2121,12 +2597,18 @@ Returns the buffer for the given server or channel." (erc-update-mode-line)) ;; Now display the buffer in a window as per user wishes. - (unless (eq buffer old-buffer) + (when (eq buffer old-buffer) (cl-assert (and connect (not target)))) + (unless (and (not erc-reconnect-display-server-buffers) + (eq buffer old-buffer)) (when erc-log-p ;; we can't log to debug buffer, it may not exist yet (message "erc: old buffer %s, switching to %s" old-buffer buffer)) - (erc-setup-buffer buffer)) + (let ((display-buffer-overriding-action + (or erc--display-buffer-overriding-action + display-buffer-overriding-action))) + (erc-setup-buffer buffer) + (run-hooks 'erc--setup-buffer-hook))) buffer)) @@ -2168,18 +2650,20 @@ in here get called with three parameters, SERVER, PORT and NICK." :type '(repeat function)) (defcustom erc-after-connect nil - "Functions called after connecting to a server. -This functions in this variable gets executed when an end of MOTD -has been received. All functions in here get called with the -parameters SERVER and NICK." + "Abnormal hook run upon establishing a logical IRC connection. +Runs on MOTD's end when `erc-server-connected' becomes non-nil. +ERC calls members with `erc-server-announced-name', falling back +to the 376/422 message's \"sender\", as well as the current nick, +as given by the 376/422 message's \"target\" parameter, which is +typically the same as that reported by `erc-current-nick'." :group 'erc-hooks :type '(repeat function)) (defun erc--ensure-url (input) (unless (string-match (rx bot "irc" (? "6") (? "s") "://") input) - (when (and (string-match (rx (? (+ any) "@") - (or (group (* (not "[")) ":" (* any)) - (+ any)) + (when (and (string-match (rx (? (+ nonl) "@") + (or (group (* (not "[")) ":" (* nonl)) + (+ nonl)) ":" (+ (not (any ":]"))) eot) input) (match-beginning 1)) @@ -2187,30 +2671,20 @@ parameters SERVER and NICK." (setq input (concat "irc://" input))) input) -;; A temporary means of addressing the problem of ERC's namesake entry -;; point defaulting to a non-TLS connection with its default server -;; (bug#60428). -(defun erc--warn-unencrypted () - ;; Remove unconditionally to avoid wrong context due to races from - ;; simultaneous dialing or aborting (e.g., via `keyboard-quit'). - (remove-hook 'erc--server-post-connect-hook #'erc--warn-unencrypted) - (when (and (process-contact erc-server-process :nowait) - (equal erc-session-server erc-default-server) - (eql erc-session-port erc-default-port)) - ;; FIXME use the autoloaded `info' instead of `Info-goto-node' in - ;; `erc-button-alist'. - (require 'info nil t) - (erc-display-error-notice - nil (concat "This connection is unencrypted. Please use `erc-tls'" - " from now on. See Info:\"(erc) connecting\" for more.")))) +(defvar erc--prompt-for-server-function nil) ;;;###autoload (defun erc-select-read-args () - "Prompt the user for values of nick, server, port, and password." - (require 'url-parse) + "Prompt for connection parameters and return them in a plist. +By default, collect `:server', `:port', `:nickname', and +`:password'. With a non-nil prefix argument, also prompt for +`:user' and `:full-name'. Also return various environmental +properties needed by entry-point commands, like `erc-tls'." (let* ((input (let ((d (erc-compute-server))) - (read-string (format "Server (default is %S): " d) - nil 'erc-server-history-list d))) + (if erc--prompt-for-server-function + (funcall erc--prompt-for-server-function) + (read-string (format-prompt "Server or URL" d) + nil 'erc-server-history-list d)))) ;; For legacy reasons, also accept a URL without a scheme. (url (url-generic-parse-url (erc--ensure-url input))) (server (url-host url)) @@ -2218,29 +2692,66 @@ parameters SERVER and NICK." (port (or (url-portspec url) (erc-compute-port (let ((d (erc-compute-port sp))) ; may be a string - (read-string (format "Port (default is %s): " d) + (read-string (format-prompt "Port" d) nil nil d))))) ;; Trust the user not to connect twice accidentally. We ;; can't use `erc-already-logged-in' to check for an existing ;; connection without modifying it to consider USER and PASS. (nick (or (url-user url) (let ((d (erc-compute-nick))) - (read-string (format "Nickname (default is %S): " d) + (read-string (format-prompt "Nickname" d) nil 'erc-nick-history-list d)))) + (user (and current-prefix-arg + (let ((d (erc-compute-user (url-user url)))) + (read-string (format-prompt "User" d) + nil nil d)))) + (full (and current-prefix-arg + (let ((d (erc-compute-full-name (url-user url)))) + (read-string (format-prompt "Full name" d) + nil nil d)))) (passwd (let* ((p (with-suppressed-warnings ((obsolete erc-password)) (or (url-password url) erc-password))) (m (if p - (format "Server password (default is %S): " p) + (format-prompt "Server password" p) "Server password (optional): "))) - (if erc-prompt-for-password (read-passwd m nil p) p)))) + (if erc-prompt-for-password (read-passwd m nil p) p))) + (opener (and (or sp (eql port erc-default-port-tls) + (and (equal server erc-default-server) + (not (string-prefix-p "irc://" input)) + (eql port erc-default-port) + (y-or-n-p "Connect using TLS instead? ") + (setq port erc-default-port-tls))) + #'erc-open-tls-stream)) + env) + (when erc-interactive-display + (push `(erc-join-buffer . ,erc-interactive-display) env)) + (when erc--display-context + (push `(erc--display-context . ,erc--display-context) env)) + (when opener + (push `(erc-server-connect-function . ,opener) env)) (when (and passwd (string= "" passwd)) (setq passwd nil)) - (when (and (equal server erc-default-server) - (eql port erc-default-port) - (not (eql port erc-default-port-tls)) ; not `erc-tls' - (not (string-prefix-p "irc://" input))) ; not yanked URL - (add-hook 'erc--server-post-connect-hook #'erc--warn-unencrypted)) - (list :server server :port port :nick nick :password passwd))) + `( :server ,server :port ,port :nick ,nick ,@(and user `(:user ,user)) + ,@(and passwd `(:password ,passwd)) ,@(and full `(:full-name ,full)) + ,@(and env `(--interactive-env-- ,env))))) + +(defmacro erc--with-entrypoint-environment (env &rest body) + "Run BODY with bindings from ENV alist." + (declare (indent 1)) + (let ((syms (make-symbol "syms")) + (vals (make-symbol "vals"))) + `(let (,syms ,vals) + (pcase-dolist (`(,k . ,v) ,env) (push k ,syms) (push v ,vals)) + (cl-progv ,syms ,vals + ,@body)))) + +;;;###autoload +(defun erc-server-select () + "Interactively connect to a server from `erc-server-alist'." + (declare (obsolete erc-tls "30.1")) + (interactive) + (let ((erc--prompt-for-server-function #'erc-networks--server-select)) + (call-interactively #'erc))) ;;;###autoload (cl-defun erc (&key (server (erc-compute-server)) @@ -2249,32 +2760,53 @@ parameters SERVER and NICK." (user (erc-compute-user)) password (full-name (erc-compute-full-name)) - id) - "ERC is a powerful, modular, and extensible IRC client. -This function is the main entry point for ERC. - -It allows selecting connection parameters, and then starts ERC. - -Non-interactively, it takes the keyword arguments - (server (erc-compute-server)) - (port (erc-compute-port)) - (nick (erc-compute-nick)) - (user (erc-compute-user)) - password - (full-name (erc-compute-full-name)) - id - -That is, if called with + id + ;; Used by interactive form + ((--interactive-env-- --interactive-env--))) + "Connect to an Internet Relay Chat SERVER on a non-TLS PORT. +Use NICK and USER, when non-nil, to inform the IRC commands of +the same name, possibly factoring in a non-nil FULL-NAME as well. +When PASSWORD is non-nil, also send an opening server password +via the \"PASS\" command. Interactively, prompt for SERVER, +PORT, NICK, and PASSWORD, along with USER and FULL-NAME when +given a prefix argument. Non-interactively, expect the rarely +needed ID parameter, when non-nil, to be a symbol or a string for +naming the server buffer and identifying the connection +unequivocally. Once connected, return the server buffer. (See +Info node `(erc) Connecting' for details about all mentioned +parameters.) + +Together with `erc-tls', this command serves as the main entry +point for ERC, the powerful, modular, and extensible IRC client. +Non-interactively, both commands accept the following keyword +arguments, with their defaults supplied by the indicated +\"compute\" functions: + + :server `erc-compute-server' + :port `erc-compute-port' + :nick `erc-compute-nick' + :user `erc-compute-user' + :password N/A + :full-name `erc-compute-full-name' + :id' N/A + +For example, when called in the following manner (erc :server \"irc.libera.chat\" :full-name \"J. Random Hacker\") -then the server and full-name will be set to those values, -whereas `erc-compute-port' and `erc-compute-nick' will be invoked -for the values of the other parameters. +ERC assigns SERVER and FULL-NAME the associated keyword values +and defers to `erc-compute-port', `erc-compute-user', and +`erc-compute-nick' for those respective parameters. -See `erc-tls' for the meaning of ID." - (interactive (erc-select-read-args)) - (erc-open server port nick full-name t password nil nil nil nil user id)) +\(fn &key SERVER PORT NICK USER PASSWORD FULL-NAME ID)" + (interactive (let ((erc--display-context `((erc-interactive-display . erc) + ,@erc--display-context))) + (erc-select-read-args))) + (unless (assq 'erc--display-context --interactive-env--) + (push '(erc--display-context . ((erc-buffer-display . erc))) + --interactive-env--)) + (erc--with-entrypoint-environment --interactive-env-- + (erc-open server port nick full-name t password nil nil nil nil user id))) ;;;###autoload (defalias 'erc-select #'erc) @@ -2288,54 +2820,47 @@ See `erc-tls' for the meaning of ID." password (full-name (erc-compute-full-name)) client-certificate - id) - "ERC is a powerful, modular, and extensible IRC client. -This function is the main entry point for ERC over TLS. - -It allows selecting connection parameters, and then starts ERC -over TLS. - -Non-interactively, it takes the keyword arguments - (server (erc-compute-server)) - (port (erc-compute-port)) - (nick (erc-compute-nick)) - (user (erc-compute-user)) - password - (full-name (erc-compute-full-name)) - client-certificate - id - -That is, if called with - - (erc-tls :server \"irc.libera.chat\" :full-name \"J. Random Hacker\") - -then the server and full-name will be set to those values, -whereas `erc-compute-port' and `erc-compute-nick' will be invoked -for the values of their respective parameters. - -CLIENT-CERTIFICATE, if non-nil, should either be a list where the -first element is the certificate key file name, and the second -element is the certificate file name itself, or t, which means -that `auth-source' will be queried for the key and the -certificate. Authenticating using a TLS client certificate is -also referred to as \"CertFP\" (Certificate Fingerprint) -authentication by various IRC networks. - -Example usage: + id + ;; Used by interactive form + ((--interactive-env-- --interactive-env--))) + "Connect to an IRC server over a TLS-encrypted connection. +Interactively, prompt for SERVER, PORT, NICK, and PASSWORD, along +with USER and FULL-NAME when given a prefix argument. +Non-interactively, also accept a CLIENT-CERTIFICATE, which should +be a list containing the file name of the certificate's key +followed by that of the certificate itself. Alternatively, +accept a value of t instead of a list, to tell ERC to query +`auth-source' for the certificate's details. + +Example client certificate (CertFP) usage: (erc-tls :server \"irc.libera.chat\" :port 6697 :client-certificate \\='(\"/home/bandali/my-cert.key\" \"/home/bandali/my-cert.crt\")) -When present, ID should be a symbol or a string to use for naming -the server buffer and identifying the connection unequivocally. -See Info node `(erc) Network Identifier' for details. Like USER -and CLIENT-CERTIFICATE, this parameter cannot be specified -interactively." - (interactive (let ((erc-default-port erc-default-port-tls)) - (erc-select-read-args))) - (let ((erc-server-connect-function 'erc-open-tls-stream)) +See the alternative entry-point command `erc' as well as Info +node `(erc) Connecting' for a fuller description of the various +parameters, like ID. + +\(fn &key SERVER PORT NICK USER PASSWORD FULL-NAME CLIENT-CERTIFICATE ID)" + (interactive + (let ((erc-default-port erc-default-port-tls) + (erc--display-context `((erc-interactive-display . erc-tls) + ,@erc--display-context))) + (erc-select-read-args))) + ;; Bind `erc-server-connect-function' to `erc-open-tls-stream' + ;; around `erc-open' when a non-default value hasn't been specified + ;; by the user or the interactive form. And don't bother checking + ;; for advice, indirect functions, autoloads, etc. + (unless (or (assq 'erc-server-connect-function --interactive-env--) + (not (eq erc-server-connect-function #'erc-open-network-stream))) + (push '(erc-server-connect-function . erc-open-tls-stream) + --interactive-env--)) + (unless (assq 'erc--display-context --interactive-env--) + (push '(erc--display-context . ((erc-buffer-display . erc-tls))) + --interactive-env--)) + (erc--with-entrypoint-environment --interactive-env-- (erc-open server port nick full-name t password nil nil nil client-certificate user id))) @@ -2352,6 +2877,25 @@ PARAMETERS should be a sequence of keywords and values, per (setq args `(,name ,buffer ,host ,port ,@p)) (apply #'open-network-stream args))) +(defun erc-open-socks-tls-stream (name buffer host service &rest parameters) + "Connect to an IRC server via SOCKS proxy over TLS. +Defer to the `socks' and `gnutls' libraries to make the actual +connection and perform TLS negotiation. Expect SERVICE to be a +TLS port number and that the plist PARAMETERS contains a +`:client-certificate' pair when necessary. Otherwise, assume the +arguments NAME, BUFFER, and HOST to be acceptable to +`open-network-stream' and that users know to check out +`erc-server-connect-function' and Info node `(erc) SOCKS' for +more info, including an important example of how to \"wrap\" this +function with SOCKS credentials." + (require 'gnutls) + (require 'socks) + (let ((proc (socks-open-network-stream name buffer host service)) + (cert-info (plist-get parameters :client-certificate))) + (gnutls-negotiate :process proc + :hostname host + :keylist (and cert-info (list cert-info))))) + ;;; Displaying error messages (defun erc-error (&rest args) @@ -2363,6 +2907,15 @@ message instead, to make debugging easier." (apply #'message args) (beep))) +(defvar erc--warnings-buffer-name nil + "Name of possibly existing alternate warnings buffer for unit tests.") + +(defun erc--lwarn (type level format-string &rest args) + "Issue a warning of TYPE and LEVEL with FORMAT-STRING and ARGS." + (let ((message (substitute-command-keys + (apply #'format-message format-string args)))) + (display-warning type message level erc--warnings-buffer-name))) + ;;; Debugging the protocol (defvar erc-debug-irc-protocol-time-format "%FT%T.%6N%z " @@ -2508,15 +3061,75 @@ If ARG is non-nil, show the *erc-protocol* buffer." ;; send interface +(defvar erc--send-action-function #'erc--send-action + "Function to display and send an outgoing CTCP ACTION message. +Called with three arguments: the submitted input, the current +target, and an `erc-server-send' FORCE flag.") + (defun erc-send-action (tgt str &optional force) "Send CTCP ACTION information described by STR to TGT." - (erc-send-ctcp-message tgt (format "ACTION %s" str) force) - (erc-display-message - nil 'input (current-buffer) - 'ACTION ?n (erc-current-nick) ?a str ?u "" ?h "")) + (funcall erc--send-action-function tgt str force)) + +;; Sending and displaying are provided separately to afford modules +;; more flexibility, e.g., to forgo displaying on the way out when +;; expecting the server to echo messages back and/or to associate +;; outgoing messages with IDs generated for `erc--ephemeral' +;; placeholders. +(defun erc--send-action-perform-ctcp (target string force) + "Send STRING to TARGET, possibly immediately, with FORCE." + (erc-send-ctcp-message target (format "ACTION %s" string) force)) + +(defvar erc--use-language-catalog-for-ctcp-action-p nil + "When non-nil, use `ACTION' entry from language catalog for /ME's. +Otherwise, use `ctcp-action' or `ctcp-action-input' from the +internal `-speaker' catalog. This is an escape hatch to restore +pre-5.6 behavior for the `font-lock-face' property of incoming +and outgoing \"CTCP ACTION\" messages, whose pre-buttonized state +was a single interval of `erc-input-face' or `erc-action-face'. +Newer modules, like `fill-wrap' and `nicks', are incompatible with +this format style. If you use this, please ask ERC to expose it +as a public variable via \\[erc-bug] or similar.") + +(defun erc--send-action-display (string) + "Display STRING as an outgoing \"CTCP ACTION\" message. +Propertize the message according to the compatibility flag +`erc--use-language-catalog-for-ctcp-action-p'." + ;; Allow hooks acting on inserted PRIVMSG and NOTICES to process us. + (let ((erc--msg-prop-overrides `((erc--ctcp . ACTION) + ,@erc--msg-prop-overrides)) + (nick (erc-current-nick))) + (if erc--use-language-catalog-for-ctcp-action-p + (progn (erc--ensure-spkr-prop nick) + (erc-display-message nil 'input (current-buffer) 'ACTION + ?n (propertize nick 'erc--speaker nick) + ?a string ?u "" ?h "")) + (let ((erc-current-message-catalog erc--message-speaker-catalog)) + (erc-display-message nil nil (current-buffer) 'ctcp-action-input + ?p (erc-get-channel-membership-prefix nick) + ?n (erc--speakerize-nick nick) ?m string))))) + +(defun erc--send-action (target string force) + "Display STRING, then send to TARGET as a \"CTCP ACTION\" message." + (erc--send-action-display string) + (erc--send-action-perform-ctcp target string force)) ;; Display interface +(defun erc--ensure-spkr-prop (nick &optional overrides) + "Add NICK as `erc--spkr' to the current \"msg props\" environment. +Prefer `erc--msg-props' over `erc--msg-prop-overrides' when both +are available. Also include any members of the alist OVERRIDES, +when present. Assume NICK itself to be free of any text props, +and return it." + (cond (erc--msg-props + (puthash 'erc--spkr nick erc--msg-props) + (dolist (entry overrides) + (puthash (car entry) (cdr entry) erc--msg-props))) + (erc--msg-prop-overrides + (setq erc--msg-prop-overrides + `((erc--spkr . ,nick) ,@overrides ,@erc--msg-prop-overrides)))) + nick) + (defun erc-string-invisible-p (string) "Check whether STRING is invisible or not. I.e. any char in it has the `invisible' property set." @@ -2527,34 +3140,236 @@ I.e. any char in it has the `invisible' property set." The default is to remove it, since it causes ERC to take up extra memory. If you have code that relies on this property, then set -this option to nil." +this option to nil. + +Note that this option is deprecated because a value of nil is +impractical in prolonged sessions with more than a few channels. +Use `erc-insert-post-hook' or similar and the helper function +`erc-find-parsed-property' and friends to stash the current +`erc-response' object as needed. And instead of using this for +debugging purposes, try `erc-debug-irc-protocol'." :type 'boolean :group 'erc) - -(defun erc-display-line-1 (string buffer) - "Display STRING in `erc-mode' BUFFER. -Auxiliary function used in `erc-display-line'. The line gets filtered to -interpret the control characters. Then, `erc-insert-pre-hook' gets called. -If `erc-insert-this' is still t, STRING gets inserted into the buffer. -Afterwards, `erc-insert-modify' and `erc-insert-post-hook' get called. -If STRING is nil, the function does nothing." +(make-obsolete-variable 'erc-remove-parsed-property + "impractical when non-nil" "30.1") + +(define-inline erc--assert-input-bounds () + (inline-quote + (progn (when (and (processp erc-server-process) + (eq (current-buffer) (process-buffer erc-server-process))) + ;; It's believed that these only need syncing immediately + ;; following the first two insertions in a server buffer. + (set-marker (process-mark erc-server-process) erc-insert-marker)) + (cl-assert (< erc-insert-marker erc-input-marker)) + (cl-assert (= (field-end erc-insert-marker) erc-input-marker))))) + +(defvar erc--merge-prop-behind-p nil + "When non-nil, put merged prop(s) behind existing.") + +(defvar erc--refresh-prompt-hook nil + "Hook called after refreshing the prompt in the affected buffer.") + +(defvar-local erc--inhibit-prompt-display-property-p nil + "Tell `erc-prompt' related functions to avoid the `display' text prop. +Modules can enable this when needing to reserve the prompt's +display property for some other purpose, such as displaying it +elsewhere, abbreviating it, etc.") + +(defconst erc--prompt-properties '( rear-nonsticky t + erc-prompt t ; t or `hidden' + field erc-prompt + front-sticky t + read-only t) + "Mandatory text properties added to ERC's prompt.") + +(defvar erc--refresh-prompt-continue-request nil + "State flag for refreshing prompt in all buffers. +When the value is zero, functions assigned to the variable +`erc-prompt' can set this to run `erc--refresh-prompt-hook' (1) +or `erc--refresh-prompt' (2) in all buffers of the server.") + +(defun erc--refresh-prompt-continue (&optional hooks-only-p) + "Ask ERC to refresh the prompt in all buffers. +Functions assigned to `erc-prompt' can call this if needing to +recreate the prompt in other buffers as well. With HOOKS-ONLY-P, +run `erc--refresh-prompt-hook' in other buffers instead of doing +a full refresh." + (when (and erc--refresh-prompt-continue-request + (zerop erc--refresh-prompt-continue-request)) + (setq erc--refresh-prompt-continue-request (if hooks-only-p 1 2)))) + +(defun erc--refresh-prompt () + "Re-render ERC's prompt when the option `erc-prompt' is a function." + (erc--assert-input-bounds) + (unless (erc--prompt-hidden-p) + (let ((erc--refresh-prompt-continue-request + (or erc--refresh-prompt-continue-request 0))) + (when (functionp erc-prompt) + (save-excursion + (goto-char (1- erc-input-marker)) + ;; Avoid `erc-prompt' (the named function), which appends a + ;; space, and `erc-display-prompt', which propertizes all + ;; but that space. + (let ((s (funcall erc-prompt)) + (p (point)) + (erc--merge-prop-behind-p t)) + (erc--merge-prop 0 (length s) 'font-lock-face 'erc-prompt-face s) + (add-text-properties 0 (length s) erc--prompt-properties s) + (insert s) + (delete-region erc-insert-marker p)))) + (run-hooks 'erc--refresh-prompt-hook) + (when-let (((> erc--refresh-prompt-continue-request 0)) + (n erc--refresh-prompt-continue-request) + (erc--refresh-prompt-continue-request -1) + (b (current-buffer))) + (erc-with-all-buffers-of-server erc-server-process + (lambda () (not (eq b (current-buffer)))) + (if (= n 1) + (run-hooks 'erc--refresh-prompt-hook) + (erc--refresh-prompt))))))) + +(defun erc--check-msg-prop (prop &optional val) + "Return PROP's value in `erc--msg-props' when populated. +If VAL is a list, return non-nil if PROP appears in VAL. If VAL +is otherwise non-nil, return non-nil if VAL compares `eq' to the +stored value. Otherwise, return the stored value." + (and-let* ((erc--msg-props) + (v (gethash prop erc--msg-props))) + (if (consp val) (memq v val) (if val (eq v val) v)))) + +(defmacro erc--get-inserted-msg-beg-at (point at-start-p) + (macroexp-let2* nil ((point point) + (at-start-p at-start-p)) + `(or (and ,at-start-p ,point) + (and-let* ((p (previous-single-property-change ,point 'erc--msg))) + (if (and (= p (1- ,point)) (get-text-property p 'erc--msg)) + p + (1- p)))))) + +(defmacro erc--get-inserted-msg-end-at (point at-start-p) + (macroexp-let2 nil point point + `(1- (next-single-property-change (if ,at-start-p (1+ ,point) ,point) + 'erc--msg nil erc-insert-marker)))) + +(defun erc--get-inserted-msg-beg (&optional point) + "Maybe return the start of message in an ERC buffer." + (erc--get-inserted-msg-beg-at (or point (setq point (point))) + (get-text-property point 'erc--msg))) + +(defun erc--get-inserted-msg-end (&optional point) + "Return the end of message in an ERC buffer. +Include any trailing white space before final newline. Expect +POINT to be less than `erc-insert-marker', and don't bother +considering `erc--insert-marker', for now." + (erc--get-inserted-msg-end-at (or point (setq point (point))) + (get-text-property point 'erc--msg))) + +(defun erc--get-inserted-msg-bounds (&optional point) + "Return bounds of message at POINT in an ERC buffer when found. +Search from POINT, when non-nil, instead of `point'. Return nil +if not found." + (let ((at-start-p (get-text-property (or point (setq point (point))) + 'erc--msg))) + (and-let* ((b (erc--get-inserted-msg-beg-at point at-start-p))) + (cons b (erc--get-inserted-msg-end-at point at-start-p))))) + +(defun erc--get-inserted-msg-prop (prop) + "Return the value of text property PROP for some message at point." + (and-let* ((stack-pos (erc--get-inserted-msg-beg (point)))) + (get-text-property stack-pos prop))) + +(defmacro erc--with-inserted-msg (&rest body) + "Simulate narrowing performed for send and insert hooks, and run BODY. +Expect callers to know that this doesn't wrap BODY in +`with-silent-modifications' or bind a temporary `erc--msg-props'." + `(when-let ((bounds (erc--get-inserted-msg-bounds))) + (save-restriction + (narrow-to-region (car bounds) (1+ (cdr bounds))) + ,@body))) + +(defun erc--traverse-inserted (beg end fn) + "Visit messages between BEG and END and run FN in narrowed buffer. +If END is a marker, possibly update its position." + (unless (markerp end) + (setq end (set-marker (make-marker) (or end erc-insert-marker)))) + (unless (eq end erc-insert-marker) + (set-marker end (min erc-insert-marker end))) + (save-excursion + (goto-char beg) + (let ((b (if (get-text-property (point) 'erc--msg) + (point) + (next-single-property-change (point) 'erc--msg nil end)))) + (while-let ((b) + ((< b end)) + (e (next-single-property-change (1+ b) 'erc--msg nil end))) + (save-restriction + (narrow-to-region b e) + (funcall fn)) + (setq b e)))) + (unless (eq end erc-insert-marker) + (set-marker end nil))) + +(defvar erc--insert-invisible-as-intangible-p nil + "When non-nil, ensure certain invisible messages are also intangible. +That is, single out any message inserted via `erc-insert-line' +that lacks a trailing newline but has a t-valued `invisible' +property anywhere along its length, and ensure it's both +`invisible' t and `intangible' t throughout. Note that this is +merely an escape hatch for accessing aberrant pre-5.6 behavior +that ERC considers a bug because it applies a practice described +as obsolete in the manual, and it does so heavy-handedly. That +the old behavior only acted when the input lacked a trailing +newline was likely accidental but is ultimately incidental. See +info node `(elisp) Special Properties' for specifics. Beware +that this flag and the behavior it restores may disappear at any +time, so if you need them, please let ERC know with \\[erc-bug].") + +(defvar erc--insert-line-function nil + "When non-nil, an alterntive to `insert' for inserting messages.") + +(defvar erc--insert-marker nil + "Internal override for `erc-insert-marker'.") + +(define-obsolete-function-alias 'erc-display-line-1 'erc-insert-line "30.1") +(defun erc-insert-line (string buffer) + "Insert STRING in an `erc-mode' BUFFER. +When STRING is nil, do nothing. Otherwise, start off by running +`erc-insert-pre-hook' in BUFFER with `erc-insert-this' bound to +t. If the latter remains non-nil afterward, insert STRING into +BUFFER, ensuring a trailing newline. After that, narrow BUFFER +around STRING, along with its final line ending, and run +`erc-insert-modify' and `erc-insert-post-hook', respectively. In +all cases, run `erc-insert-done-hook' unnarrowed before exiting, +and update positions in `buffer-undo-list'. + +In general, expect to be called from a higher-level insertion +function, like `erc-display-message', especially when modules +should consider STRING as a candidate for formatting with +enhancements like indentation, fontification, timestamping, etc. +Otherwise, when called directly, allow built-in modules to ignore +STRING, which may make it appear incongruous in situ (unless +preformatted or anticipated by third-party members of the various +modification hooks)." (when string (with-current-buffer (or buffer (process-buffer erc-server-process)) - (let ((insert-position (or (marker-position erc-insert-marker) - (point-max)))) - (let ((string string) ;; FIXME! Can this be removed? - (buffer-undo-list t) + (let (insert-position) + ;; Initialize ^ below to thwart rogue `erc-insert-pre-hook' + ;; members that dare to modify the buffer's length. + (let ((buffer-undo-list t) (inhibit-read-only t)) - (unless (string-match "\n$" string) + (unless (string-suffix-p "\n" string) (setq string (concat string "\n")) - (when (erc-string-invisible-p string) + (when (and erc--insert-invisible-as-intangible-p + (erc-string-invisible-p string)) (erc-put-text-properties 0 (length string) '(invisible intangible) string))) - (erc-log (concat "erc-display-line: " string + (erc-log (concat "erc-display-message: " string (format "(%S)" string) " in buffer " (format "%s" buffer))) (setq erc-insert-this t) (run-hook-with-args 'erc-insert-pre-hook string) + (setq insert-position (marker-position (or erc--insert-marker + erc-insert-marker))) (if (null erc-insert-this) ;; Leave erc-insert-this set to t as much as possible. Fran ;; Litterio <franl> has seen erc-insert-this set to nil while @@ -2565,7 +3380,10 @@ If STRING is nil, the function does nothing." (save-restriction (widen) (goto-char insert-position) - (insert-before-markers string) + (if erc--insert-line-function + (funcall erc--insert-line-function string) + (insert string)) + (erc--assert-input-bounds) ;; run insertion hook, with point at restored location (save-restriction (narrow-to-region insert-position (point)) @@ -2573,9 +3391,17 @@ If STRING is nil, the function does nothing." (run-hooks 'erc-insert-post-hook) (when erc-remove-parsed-property (remove-text-properties (point-min) (point-max) - '(erc-parsed nil)))))))) + '(erc-parsed nil tags nil))) + (cl-assert (> (- (point-max) (point-min)) 1)) + (let ((props (if erc--msg-props + (erc--order-text-properties-from-hash + erc--msg-props) + '(erc--msg unknown)))) + (add-text-properties (point-min) (1+ (point-min)) props))) + (erc--refresh-prompt))))) (run-hooks 'erc-insert-done-hook) - (erc-update-undo-list (- (or (marker-position erc-insert-marker) + (erc-update-undo-list (- (or (marker-position (or erc--insert-marker + erc-insert-marker)) (point-max)) insert-position)))))) @@ -2609,37 +3435,211 @@ If STRING is nil, the function does nothing." "Check if NICK is a valid IRC nickname." (string-match (concat "\\`" erc-valid-nick-regexp "\\'") nick)) -(defun erc-display-line (string &optional buffer) - "Display STRING in the ERC BUFFER. -All screen output must be done through this function. If BUFFER is nil -or omitted, the default ERC buffer for the `erc-session-server' is used. -The BUFFER can be an actual buffer, a list of buffers, `all' or `active'. -If BUFFER = `all', the string is displayed in all the ERC buffers for the -current session. `active' means the current active buffer -\(`erc-active-buffer'). If the buffer can't be resolved, the current -buffer is used. `erc-display-line-1' is used to display STRING. - -If STRING is nil, the function does nothing." - (let (new-bufs) +(defun erc--route-insertion (string buffer) + "Insert STRING in BUFFER. +See `erc-display-message' for acceptable BUFFER types." + (let (seen msg-props) (dolist (buf (cond ((bufferp buffer) (list buffer)) - ((listp buffer) buffer) + ((consp buffer) + (setq msg-props erc--msg-props) + buffer) ((processp buffer) (list (process-buffer buffer))) ((eq 'all buffer) ;; Hmm, or all of the same session server? (erc-buffer-list nil erc-server-process)) - ((and (eq 'active buffer) (erc-active-buffer)) - (list (erc-active-buffer))) + ((and-let* (((eq 'active buffer)) + (b (erc-active-buffer))) + (list b))) ((erc-server-buffer-live-p) (list (process-buffer erc-server-process))) (t (list (current-buffer))))) (when (buffer-live-p buf) - (erc-display-line-1 string buf) - (push buf new-bufs))) - (when (null new-bufs) - (erc-display-line-1 string (if (erc-server-buffer-live-p) - (process-buffer erc-server-process) - (current-buffer)))))) + (when msg-props + (setq erc--msg-props (copy-hash-table msg-props))) + (erc-insert-line string buf) + (setq seen t))) + (unless (or seen (null buffer)) + (erc--route-insertion string nil)))) + +(defun erc-display-line (string &optional buffer) + "Insert STRING in BUFFER as a plain \"local\" message. +Take pains to ensure modification hooks see messages created by +the old pattern (erc-display-line (erc-make-notice) my-buffer) as +being equivalent to a `erc-display-message' TYPE of `notice'." + (let ((erc--msg-prop-overrides erc--msg-prop-overrides)) + (when (eq 'erc-notice-face (get-text-property 0 'font-lock-face string)) + (unless (assq 'erc--msg erc--msg-prop-overrides) + (push '(erc--msg . notice) erc--msg-prop-overrides))) + (erc-display-message nil nil buffer string))) + +(defvar erc--merge-text-properties-p nil + "Non-nil when `erc-put-text-property' defers to `erc--merge-prop'.") + +;; To save space, we could maintain a map of all readable property +;; values and optionally dispense archetypal constants in their place +;; in order to ensure all occurrences of some list (a b) across all +;; text-properties in all ERC buffers are actually the same object. +(defun erc--merge-prop (from to prop val &optional object cache-fn) + "Combine existing PROP values with VAL between FROM and TO in OBJECT. +For spans where PROP is non-nil, cons VAL onto the existing +value, ensuring a proper list. Otherwise, just set PROP to VAL. +When VAL is itself a list, prepend its members onto an existing +value. Call CACHE-FN, when given, with the new value for prop. +It must return a suitable replacement or the same value. See +also `erc-button-add-face'." + (let ((old (get-text-property from prop object)) + (pos from) + (end (next-single-property-change from prop object to)) + new) + (while (< pos to) + (setq new (if old + ;; Can't `nconc' without more info. + (if erc--merge-prop-behind-p + `(,@(ensure-list old) ,@(ensure-list val)) + (if (listp val) + (append val (ensure-list old)) + (cons val (ensure-list old)))) + val)) + (when cache-fn + (setq new (funcall cache-fn new))) + (put-text-property pos end prop new object) + (setq pos end + old (get-text-property pos prop object) + end (next-single-property-change pos prop object to))))) + +(defun erc--remove-from-prop-value-list (from to prop val &optional object) + "Remove VAL from text prop value between FROM and TO. +If current value is VAL itself, remove the property entirely. +When VAL is a list, act as if this function were called +repeatedly with VAL set to each of VAL's members." + (let ((old (get-text-property from prop object)) + (pos from) + (end (next-single-property-change from prop object to)) + new) + (while (< pos to) + (when old + (if (setq new (and (consp old) (if (consp val) + (seq-difference old val) + (remq val old)))) + (put-text-property pos end prop + (if (cdr new) new (car new)) object) + (when (pcase val + ((pred consp) (or (consp old) (memq old val))) + (_ (if (consp old) (memq val old) (eq old val)))) + (remove-text-properties pos end (list prop nil) object)))) + (setq pos end + old (get-text-property pos prop object) + end (next-single-property-change pos prop object to))))) + +(defun erc--reserve-important-text-props (beg end plist &optional object) + "Record text-property pairs in PLIST as important between BEG and END. +Also mark the message being inserted as containing these important props +so modules performing destructive modifications can later restore them. +Expect to run in a narrowed buffer at message-insertion time." + (when erc--msg-props + (let ((existing (erc--check-msg-prop 'erc--important-prop-names))) + (puthash 'erc--important-prop-names (cl-union existing (map-keys plist)) + erc--msg-props))) + (erc--merge-prop beg end 'erc--important-props plist object)) + +(defun erc--restore-important-text-props (props &optional beg end) + "Restore PROPS where recorded in the accessible portion of the buffer. +Expect to run in a narrowed buffer at message-insertion time. Limit the +effect to the region between buffer positions BEG and END, when non-nil. + +Callers should be aware that this function fails if the property +`erc--important-props' has an empty value almost anywhere along the +affected region. Use the function `erc--remove-from-prop-value-list' to +ensure that props with empty values are excised completely." + (when-let ((registered (erc--check-msg-prop 'erc--important-prop-names)) + (present (seq-intersection props registered)) + (b (or beg (point-min))) + (e (or end (point-max)))) + (while-let + (((setq b (text-property-not-all b e 'erc--important-props nil))) + (val (get-text-property b 'erc--important-props)) + (q (next-single-property-change b 'erc--important-props nil e))) + (while-let ((k (pop val)) + (v (pop val))) + (when (memq k present) + (put-text-property b q k v))) + (setq b q)))) + +(defvar erc-legacy-invisible-bounds-p nil + "Whether to hide trailing rather than preceding newlines. +Beginning in ERC 5.6, invisibility extends from a message's +preceding newline to its last non-newline character.") +(make-obsolete-variable 'erc-legacy-invisible-bounds-p + "decremented interval now permanent" "30.1") + +(defun erc--hide-message (value) + "Apply `invisible' text-property with VALUE to current message. +Expect to run in a narrowed buffer during message insertion. +Begin the invisible interval at the previous message's trailing +newline and end before the current message's. If the preceding +message ends in a double newline or there is no previous message, +don't bother including the preceding newline." + (if erc-legacy-invisible-bounds-p + ;; Before ERC 5.6, this also used to add an `intangible' + ;; property, but the docs say it's now obsolete. + (erc--merge-prop (point-min) (point-max) 'invisible value) + (let ((beg (point-min)) + (end (point-max))) + (save-restriction + (widen) + (when (or (<= beg 4) (= ?\n (char-before (- beg 2)))) + (cl-incf beg)) + (erc--merge-prop (1- beg) (1- end) 'invisible value))))) + +(defun erc--toggle-hidden (prop arg) + "Toggle invisibility for spec member PROP. +Treat ARG in a manner similar to mode toggles defined by +`define-minor-mode'." + (when arg + (setq arg (prefix-numeric-value arg))) + (if (memq prop (ensure-list buffer-invisibility-spec)) + (unless (natnump arg) + (remove-from-invisibility-spec prop)) + (when (or (not arg) (natnump arg)) + (add-to-invisibility-spec prop)))) + +(defun erc--delete-inserted-message (beg-or-point &optional end) + "Remove message between BEG and END. +Expect BEG and END to match bounds as returned by the macro +`erc--get-inserted-msg-bounds'. Ensure all markers residing at +the start of the deleted message end up at the beginning of the +subsequent message." + (let ((beg beg-or-point)) + (save-restriction + (widen) + (unless end + (setq end (erc--get-inserted-msg-bounds beg-or-point) + beg (pop end))) + (with-silent-modifications + (if erc-legacy-invisible-bounds-p + (delete-region beg (1+ end)) + (save-excursion + (goto-char beg) + (insert-before-markers + (substring (delete-and-extract-region (1- (point)) (1+ end)) + -1)))))))) + +(defvar erc--ranked-properties + '(erc--msg erc--spkr erc--ts erc--cmd erc--ctcp erc--ephemeral)) + +(defun erc--order-text-properties-from-hash (table) + "Return a plist of text props from items in TABLE. +Ensure props in `erc--ranked-properties' appear last and in +reverse order so they end up sorted in buffer interval plists for +retrieval by `text-properties-at' and friends." + (let (out) + (dolist (k erc--ranked-properties) + (when-let ((v (gethash k table))) + (remhash k table) + (setq out (nconc (list k v) out)))) + (maphash (lambda (k v) (setq out (nconc (list k v) out))) table) + out)) (defun erc-display-message-highlight (type string) "Highlight STRING according to TYPE, where erc-TYPE-face is an ERC face. @@ -2652,7 +3652,7 @@ See also `erc-make-notice'." 0 (length string) 'font-lock-face (or (intern-soft (concat "erc-" (symbol-name type) "-face")) - "erc-default-face") + 'erc-default-face) string) string))) @@ -2854,35 +3854,87 @@ returns non-nil." (defun erc-display-message (parsed type buffer msg &rest args) "Display MSG in BUFFER. -ARGS, PARSED, and TYPE are used to format MSG sensibly. - -See also `erc-format-message' and `erc-display-line'." - (let ((string (if (symbolp msg) - (apply #'erc-format-message msg args) - msg)) - (erc-message-parsed parsed)) +Insert MSG or text derived from MSG into an ERC buffer, possibly +after applying formatting by way of either a `format-spec' known +to a message-catalog entry or a TYPE known to a specialized +string handler. Additionally, derive metadata, faces, and other +text properties from the various overloaded parameters, such as +PARSED, when it's an `erc-response' object, and MSG, when it's a +key (symbol) for a \"message catalog\" entry. Expect ARGS, when +applicable, to be `format-spec' args known to such an entry, and +TYPE, when non-nil, to be a symbol handled by +`erc-display-message-highlight' (necessarily accompanied by a +string MSG). Expect BUFFER to be among the sort accepted by the +function `erc-display-line'. + +When non-nil, expect BUFFER to be a live `erc-mode' buffer, a +list of such buffers, or the symbols `all' or `active'. If +`all', insert STRING in all buffers for the current session. If +`active', defer to the function `erc-active-buffer', which may +return the session's server buffer if the previously active +buffer has been killed. If BUFFER is nil or a network process, +pretend it's set to the appropriate server buffer. Otherwise, +use the current buffer. + +When TYPE is a list of symbols, call handlers from left to right +without influencing how they behave when encountering existing +faces. As of ERC 5.6, expect a TYPE of (notice error) to insert +MSG with `font-lock-face' as `erc-error-face' throughout. +However, when the list of symbols begins with t, tell compatible +handlers to compose rather than clobber faces. For example, +expect a TYPE of (t notice error) to result in `font-lock-face' +being (erc-error-face erc-notice-face) throughout MSG when +`erc-notice-highlight-type' is left at its default, `all'. + +As of ERC 5.6, assume third-party code will use this function +instead of lower-level ones, like `erc-insert-line', to insert +arbitrary informative messages as if sent by the server. That +is, tell modules to treat a \"local\" message for which PARSED is +nil like any other server-sent message." + (let* ((erc--msg-props + (or erc--msg-props + (let ((table (make-hash-table)) + (cmd (and parsed (erc--get-eq-comparable-cmd + (erc-response.command parsed))))) + (puthash 'erc--msg + (cond ((and msg (symbolp msg)) msg) + (type (pcase type + ((pred symbolp) type) + ((pred listp) + (intern (mapconcat #'prin1-to-string + type "-"))) + (_ 'unknown))) + (t 'unknown)) + table) + (when cmd + (puthash 'erc--cmd cmd table)) + (when erc--msg-prop-overrides + (pcase-dolist (`(,k . ,v) (reverse erc--msg-prop-overrides)) + (when v (puthash k v table)))) + table))) + (erc-message-parsed parsed) + (string (if (symbolp msg) (apply #'erc-format-message msg args) msg))) (setq string (cond ((null type) string) ((listp type) - (mapc (lambda (type) - (setq string - (erc-display-message-highlight type string))) - type) + (let ((erc--merge-text-properties-p + (and (eq (car type) t) (setq type (cdr type))))) + (dolist (type type) + (setq string (erc-display-message-highlight type string)))) string) ((symbolp type) (erc-display-message-highlight type string)))) (if (not (erc-response-p parsed)) - (erc-display-line string buffer) + (erc--route-insertion string buffer) (unless (erc-hide-current-message-p parsed) (erc-put-text-property 0 (length string) 'erc-parsed parsed string) - (erc-put-text-property 0 (length string) 'rear-sticky t string) (when (erc-response.tags parsed) (erc-put-text-property 0 (length string) 'tags (erc-response.tags parsed) string)) - (erc-display-line string buffer))))) + (erc--route-insertion string buffer))))) (defun erc-message-type-member (position list) "Return non-nil if the erc-parsed text-property at POSITION is in LIST. @@ -2892,17 +3944,39 @@ present." (let ((prop-val (erc-get-parsed-vector position))) (and prop-val (member (erc-response.command prop-val) list)))) -(defvar-local erc-send-input-line-function 'erc-send-input-line - "Function for sending lines lacking a leading user command. -When a line typed into a buffer contains an explicit command, like /msg, -a corresponding handler (here, erc-cmd-MSG) is called. But lines typed -into a channel or query buffer already have an implicit target and -command (PRIVMSG). This function is called on such occasions and also -for special purposes (see erc-dcc.el).") +(defvar erc--called-as-input-p nil + "Non-nil when a user types a \"/slash\" command. +Remains bound until `erc-cmd-SLASH' returns.") + +(defvar erc--current-line-input-split nil + "Current `erc--input-split' instance when processing user input. +This is for special cases in which a \"slash\" command needs +details about the input it's handling or needs to detect whether +it's been dispatched by `erc-send-current-line'.") + +(defvar erc--allow-empty-outgoing-lines-p nil + "Flag to opt out of last-minute padding of empty lines. +Useful to extensions, like `multiline', and for interop with +IRC-adjacent protocols.") + +(defvar-local erc-send-input-line-function #'erc-send-input-line + "Function for sending lines lacking a leading \"slash\" command. +When prompt input starts with a \"slash\" command, like \"/MSG\", +ERC calls a corresponding handler, like `erc-cmd-MSG'. But +normal \"chat\" input also needs processing, for example, to +convert it into a proper IRC command. ERC calls this variable's +value to perform that task, which, by default, simply involves +constructing a \"PRIVMSG\" with the current channel or query +partner as the target. Some libraries, like `erc-dcc', use this +for other purposes.") (defun erc-send-input-line (target line &optional force) "Send LINE to TARGET." - (when (string= line "\n") + (when-let ((target) + (cmem (erc-get-channel-member (erc-current-nick)))) + (setf (erc-channel-user-last-message-time (cdr cmem)) + (erc-compat--current-lisp-time))) + (when (and (not erc--allow-empty-outgoing-lines-p) (string= line "\n")) (setq line " \n")) (erc-message "PRIVMSG" (concat target " " line) force)) @@ -2930,22 +4004,26 @@ erc-cmd-FOO, this returns a string /FOO." command-name))) (defun erc-process-input-line (line &optional force no-command) - "Translate LINE to an RFC1459 command and send it based. -Returns non-nil if the command is actually sent to the server, and nil -otherwise. - -If the command in the LINE is not bound as a function `erc-cmd-<COMMAND>', -it is passed to `erc-cmd-default'. If LINE is not a command (i.e. doesn't -start with /<COMMAND>) then it is sent as a message. - -An optional FORCE argument forces sending the line when flood -protection is in effect. The optional NO-COMMAND argument prohibits -this function from interpreting the line as a command." + "Dispatch a slash-command or chat-input handler from user-input LINE. +If simplistic validation fails, print an error and return nil. +Otherwise, defer to an appropriate handler. For \"slash\" commands, +like \"/JOIN\", expect a handler, like `erc-cmd-JOIN', to return non-nil +if LINE is fit for echoing as a command line when executing scripts. +For normal chat input, expect a handler to return non-nil if a message +was successfully processed as an outgoing \"PRIVMSG\". If LINE is a +slash command, and ERC can't find a corresponding handler of the form +`erc-cmd-<COMMAND>', pass LINE to `erc-cmd-default', treating it as a +catch-all handler. Otherwise, for normal chat input, pass LINE and the +boolean argument FORCE to `erc-send-input-line-function'. With a +non-nil NO-COMMAND, always treat LINE as normal chat input rather than a +slash command." (let ((command-list (erc-extract-command-from-line line))) (if (and command-list (not no-command)) (let* ((cmd (nth 0 command-list)) - (args (nth 1 command-list))) + (args (nth 1 command-list)) + (erc--called-as-input-p t)) + (erc--server-last-reconnect-display-reset (erc-server-buffer)) (condition-case nil (if (listp args) (apply cmd args) @@ -2961,23 +4039,85 @@ this function from interpreting the line as a command." (let ((r (erc-default-target))) (if r (funcall erc-send-input-line-function r line force) - (erc-display-message nil 'error (current-buffer) 'no-target) + (erc-display-message nil '(notice error) (current-buffer) 'no-target) nil))))) +(defconst erc--shell-parse-regexp + (rx (or (+ (not (any ?\s ?\t ?\n ?\\ ?\" ?' ?\;))) + (: ?' (group (* (not ?'))) (? ?')) + (: ?\" (group (* (or (not (any ?\" ?\\)) (: ?\\ nonl)))) (? ?\")) + (: ?\\ (group (? (or nonl ?\n))))))) + +(defun erc--split-string-shell-cmd (string) + "Parse whitespace-separated arguments in STRING." + ;; From `shell--parse-pcomplete-arguments' and friends. Quirk: + ;; backslash-escaped characters appearing within spans of double + ;; quotes are unescaped. + (with-temp-buffer + (insert string) + (let ((end (point)) + args) + (goto-char (point-min)) + (while (and (skip-chars-forward " \t") (< (point) end)) + (let (arg) + (while (looking-at erc--shell-parse-regexp) + (goto-char (match-end 0)) + (cond ((match-beginning 3) ; backslash escape + (push (if (= (match-beginning 3) (match-end 3)) + "\\" + (match-string 3)) + arg)) + ((match-beginning 2) ; double quote + (push (replace-regexp-in-string (rx ?\\ (group nonl)) + "\\1" (match-string 2)) + arg)) + ((match-beginning 1) ; single quote + (push (match-string 1) arg)) + (t (push (match-string 0) arg)))) + (push (string-join (nreverse arg)) args))) + (nreverse args)))) + + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Input commands handlers ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(defun erc-cmd-AMSG (line) - "Send LINE to all channels of the current server that you are on." - (interactive "sSend to all channels you're on: ") - (setq line (erc-trim-string line)) +(defun erc--connected-and-joined-p () + (and (erc--current-buffer-joined-p) + erc-server-connected)) + +(defun erc-cmd-GMSG (line) + "Send LINE to all channels on all networks you are on." + (setq line (string-remove-prefix " " line)) (erc-with-all-buffers-of-server nil - (lambda () - (erc-channel-p (erc-default-target))) + #'erc--connected-and-joined-p + (erc-send-message line))) +(put 'erc-cmd-GMSG 'do-not-parse-args t) + +(defun erc-cmd-AMSG (line) + "Send LINE to all channels of the current network. +Interactively, prompt for the line of text to send." + (interactive "sSend to all channels on this network: ") + (setq line (string-remove-prefix " " line)) + (erc-with-all-buffers-of-server erc-server-process + #'erc--connected-and-joined-p (erc-send-message line))) (put 'erc-cmd-AMSG 'do-not-parse-args t) +(defun erc-cmd-GME (line) + "Send LINE as an action to all channels on all networks you are on." + (erc-with-all-buffers-of-server nil + #'erc--connected-and-joined-p + (erc-cmd-ME line))) +(put 'erc-cmd-GME 'do-not-parse-args t) + +(defun erc-cmd-AME (line) + "Send LINE as an action to all channels on the current network." + (erc-with-all-buffers-of-server erc-server-process + #'erc--connected-and-joined-p + (erc-cmd-ME line))) +(put 'erc-cmd-AME 'do-not-parse-args t) + (defun erc-cmd-SAY (line) "Send LINE to the current query or channel as a message, not a command. @@ -2987,9 +4127,7 @@ need this when pasting multiple lines of text." (if (string-match "^\\s-*$" line) nil (string-match "^ ?\\(.*\\)" line) - (let ((msg (match-string 1 line))) - (erc-display-msg msg) - (erc-process-input-line msg nil t)))) + (erc-send-message (match-string 1 line) nil))) (put 'erc-cmd-SAY 'do-not-parse-args t) (defun erc-cmd-SET (line) @@ -3064,6 +4202,8 @@ returns the time spec converted to a number of seconds." (string-to-number period)) ;; Parse as a time spec. (t + (require 'time-date) + (require 'iso8601) (let ((time (condition-case nil (iso8601-parse-duration (concat (cond @@ -3100,16 +4240,14 @@ If no USER argument is specified, list the contents of `erc-ignore-list'." (run-at-time timeout nil (lambda () (erc--unignore-user user buffer)))) - (erc-display-line - (erc-make-notice (format "Now ignoring %s" user)) - 'active) + (erc-display-message nil 'notice 'active + (format "Now ignoring %s" user)) (erc-with-server-buffer (add-to-list 'erc-ignore-list user)))) (if (null (erc-with-server-buffer erc-ignore-list)) - (erc-display-line (erc-make-notice "Ignore list is empty") 'active) - (erc-display-line (erc-make-notice "Ignore list:") 'active) + (erc-display-message nil 'notice 'active "Ignore list is empty") + (erc-display-message nil 'notice 'active "Ignore list:") (mapc (lambda (item) - (erc-display-line (erc-make-notice item) - 'active)) + (erc-display-message nil 'notice 'active item)) (erc-with-server-buffer erc-ignore-list)))) t) @@ -3123,9 +4261,8 @@ If no USER argument is specified, list the contents of `erc-ignore-list'." (unless (y-or-n-p (format "Remove this regexp (%s)? " ignored-nick)) (setq ignored-nick nil)) - (erc-display-line - (erc-make-notice (format "%s is not currently ignored!" user)) - 'active))) + (erc-display-message nil 'notice 'active + (format "%s is not currently ignored!" user)))) (when ignored-nick (erc--unignore-user user (current-buffer)))) t) @@ -3133,16 +4270,26 @@ If no USER argument is specified, list the contents of `erc-ignore-list'." (defun erc--unignore-user (user buffer) (when (buffer-live-p buffer) (with-current-buffer buffer - (erc-display-line - (erc-make-notice (format "No longer ignoring %s" user)) - 'active) + (erc-display-message nil 'notice 'active + (format "No longer ignoring %s" user)) (erc-with-server-buffer (setq erc-ignore-list (delete user erc-ignore-list)))))) +(defvar erc--pre-clear-functions nil + "Abnormal hook run when truncating buffers. +Called with position indicating boundary of interval to be excised.") + (defun erc-cmd-CLEAR () - "Clear the window content." - (let ((inhibit-read-only t)) - (delete-region (point-min) (line-beginning-position))) + "Clear messages in current buffer after informing active modules. +Expect modules to perform housekeeping tasks to withstand the +disruption. When called from lisp code, only clear messages up +to but not including the one occupying the current line." + (with-silent-modifications + (let ((max (if (>= (point) erc-insert-marker) + (1- erc-insert-marker) + (or (erc--get-inserted-msg-beg (point)) (pos-bol))))) + (run-hook-with-args 'erc--pre-clear-functions max) + (delete-region (point-min) max))) t) (put 'erc-cmd-CLEAR 'process-not-needed t) @@ -3213,7 +4360,7 @@ VERSION and so on. It is called with ARGS." (erc-send-ctcp-message nick str) t)) -(defun erc-cmd-HELP (&optional func) +(defun erc-cmd-HELP (&optional func &rest rest) "Popup help information. If FUNC contains a valid function or variable, help about that @@ -3246,6 +4393,10 @@ For a list of user commands (/join /part, ...): nil))))) (if sym (cond + ((get sym 'erc--cmd-help) + (when (autoloadp (symbol-function sym)) + (autoload-do-load (symbol-function sym))) + (apply (get sym 'erc--cmd-help) rest)) ((boundp sym) (describe-variable sym)) ((fboundp sym) (describe-function sym)) (t nil)) @@ -3369,11 +4520,22 @@ the one with host foo would win." (plist-get (car sorted) :secret)))) (defun erc-auth-source-search (&rest plist) - "Call `auth-source-search', possibly with keyword params in PLIST." + "Call `auth-source-search', possibly with keyword params in PLIST. +If the search signals an error before returning, `warn' the user +and ask whether to continue connecting anyway." ;; These exist as separate helpers in case folks should find them ;; useful. If that's you, please request that they be exported. - (apply #'erc--auth-source-search - (apply #'erc--auth-source-determine-params-merge plist))) + (condition-case err + (apply #'erc--auth-source-search + (apply #'erc--auth-source-determine-params-merge plist)) + (error + (erc--lwarn '(erc auth-source) :error + "Problem querying `auth-source': %S. See %S for more." + (error-message-string err) + '(info "(erc) auth-source Troubleshooting")) + (when (or noninteractive + (not (y-or-n-p "Ignore auth-source error and continue? "))) + (signal (car err) (cdr err)))))) (defun erc-server-join-channel (server channel &optional secret) "Join CHANNEL, optionally with SECRET. @@ -3412,7 +4574,24 @@ were most recently invited. See also `invitation'." ((with-current-buffer existing (erc-get-channel-user (erc-current-nick))))) (switch-to-buffer existing) - (setq erc--server-last-reconnect-count 0) + (when-let* ; bind `erc-join-buffer' when /JOIN issued + ((erc--called-as-input-p) + (fn (lambda (proc parsed) + (when-let* ; `fn' wrapper already removed from hook + (((equal (car (erc-response.command-args parsed)) + channel)) + (sn (erc-extract-nick (erc-response.sender parsed))) + ((erc-nick-equal-p sn (erc-current-nick))) + (erc-join-buffer (or erc-interactive-display + erc-join-buffer)) + (erc--display-context `((erc-interactive-display + . /JOIN) + ,@erc--display-context))) + (run-hook-with-args-until-success + 'erc-server-JOIN-functions proc parsed) + t)))) + (erc-with-server-buffer + (erc-once-with-server-event "JOIN" fn))) (erc-server-join-channel nil chnl key)))) t) @@ -3592,12 +4771,10 @@ See `erc-cmd-WHOIS' for more details." (string-to-number (cl-third (erc-response.command-args parsed))))) - (erc-display-line - (erc-make-notice + (erc-display-message nil 'notice origbuf (format "%s has been idle for %s." (erc-string-no-properties nick) (erc-seconds-to-string idleseconds))) - origbuf) t))) 'erc-server-317-functions) symlist) @@ -3661,17 +4838,68 @@ the matching is case-sensitive." (put 'erc-cmd-LASTLOG 'do-not-parse-args t) (put 'erc-cmd-LASTLOG 'process-not-needed t) +(defvar erc--send-message-nested-function #'erc--send-message-nested + "Function for inserting and sending slash-command generated text. +When a command like /SV or /SAY modifies or replaces command-line +input originally submitted at the prompt, `erc-send-message' +performs additional processing to ensure said input is fit for +inserting and sending given this \"nested\" meta context. This +interface variable exists because modules extending fundamental +insertion and sending operations need a say in this processing as +well.") + (defun erc-send-message (line &optional force) "Send LINE to the current channel or user and display it. See also `erc-message' and `erc-display-line'." - (erc-message "PRIVMSG" (concat (erc-default-target) " " line) force) - (erc-display-line - (concat (erc-format-my-nick) line) - (current-buffer)) + (if (erc--input-split-p erc--current-line-input-split) + (funcall erc--send-message-nested-function line force) + (erc--send-message-external line force))) + +(defun erc--send-message-external (line force) + "Send a \"PRIVMSG\" to the default target with optional FORCE. +Expect caller to bind `erc-default-recipients' if needing to +specify a status-prefixed target." + ;; Almost like an echoed message, but without the `erc--cmd'. + (let* ((erc-current-message-catalog erc--message-speaker-catalog) + (target (erc-default-target)) + (erc--msg-prop-overrides `((erc--tmp) ,@erc--msg-prop-overrides)) + ;; This util sets the `erc--spkr' property in ^. + (trimmed (erc--statusmsg-target target)) + (stmsgindc (and trimmed (substring target 0 1))) + (queryp (and erc--target (not (erc--target-channel-p erc--target)))) + (args (erc--determine-speaker-message-format-args + (erc-current-nick) line queryp 'privmsgp 'inputp + stmsgindc 'prefix))) + (erc-message "PRIVMSG" (concat target " " line) force) + (push (cons 'erc--msg (car args)) erc--msg-prop-overrides) + (apply #'erc-display-message nil nil (current-buffer) args)) ;; FIXME - treat multiline, run hooks, or remove me? + ;; FIXME explain this ^ in more detail or remove. + t) + +(defun erc--send-message-nested (input-line force) + "Process string INPUT-LINE almost as if it's normal chat input. +Expect INPUT-LINE to differ from the `string' slot of the calling +context's `erc--current-line-input-split' object because the +latter is likely a slash command invocation whose handler +generated INPUT-LINE. Before inserting INPUT-LINE, split it and +run `erc-send-modify-hook' and `erc-send-post-hook' on each +actual outgoing line. Forgo input validation because this isn't +interactive input, and skip `erc-send-completed-hook' because it +will run just before the outer `erc-send-current-line' call +returns." + (let* ((erc-flood-protect (not force)) + (lines-obj (erc--make-input-split input-line))) + (setf (erc--input-split-refoldp lines-obj) t + (erc--input-split-cmdp lines-obj) nil) + (erc--send-input-lines (erc--run-send-hooks lines-obj))) t) +;; FIXME if the user types /MODE<RET>, LINE becomes "\n", which +;; matches the pattern, so "\n" is sent to the server. Perhaps +;; instead of `do-not-parse-args', this should just join &rest +;; arguments. (defun erc-cmd-MODE (line) "Change or display the mode value of a channel or user. The first word specifies the target. The rest is the mode string @@ -3686,7 +4914,7 @@ A list of valid mode strings for Libera.Chat may be found at ((string-match "^\\s-\\(.*\\)$" line) (let ((s (match-string 1 line))) (erc-log (format "cmd: MODE: %s" s)) - (erc-server-send (concat "MODE " line))) + (erc-server-send (concat "MODE " s))) t) (t nil))) (put 'erc-cmd-MODE 'do-not-parse-args t) @@ -3711,6 +4939,7 @@ The rest of LINE is the message to send." The rest of LINE is the message to send." (erc-message "SQUERY" line)) +(put 'erc-cmd-SQUERY 'do-not-parse-args t) (defun erc-cmd-NICK (nick) "Change current nickname to NICK." @@ -3753,7 +4982,7 @@ Otherwise leave the channel indicated by LINE." (format "PART %s" ch) (format "PART %s :%s" ch reason)) nil ch)) - (erc-display-message nil 'error (current-buffer) 'no-target))) + (erc-display-message nil '(notice error) (current-buffer) 'no-target))) t) (t nil))) (put 'erc-cmd-PART 'do-not-parse-args t) @@ -3776,27 +5005,10 @@ just as you provided it. Use this command with care!" (t nil))) (put 'erc-cmd-QUOTE 'do-not-parse-args t) -(defcustom erc-query-display 'window - "How to display query buffers when using the /QUERY command to talk to someone. - -The default behavior is to display the message in a new window -and bring it to the front. See the documentation for -`erc-join-buffer' for a description of the available choices. - -See also `erc-auto-query' to decide how private messages from -other people should be displayed." - :group 'erc-query - :type '(choice (const :tag "Split window and select" window) - (const :tag "Split window, don't select" window-noselect) - (const :tag "New frame" frame) - (const :tag "Bury in new buffer" bury) - (const :tag "Use current buffer" buffer) - (const :tag "Use current buffer" t))) - (defun erc-cmd-QUERY (&optional user) "Open a query with USER. How the query is displayed (in a new window, frame, etc.) depends -on the value of `erc-query-display'." +on the value of `erc-interactive-display'." ;; FIXME: The doc string used to say at the end: ;; "If USER is omitted, close the current query buffer if one exists ;; - except this is broken now ;-)" @@ -3807,8 +5019,10 @@ on the value of `erc-query-display'." (unless user ;; currently broken, evil hack to display help anyway ;(erc-delete-query)))) - (signal 'wrong-number-of-arguments "")) - (let ((erc-join-buffer erc-query-display)) + (signal 'wrong-number-of-arguments '(erc-cmd-QUERY 0))) + (let ((erc-join-buffer erc-interactive-display) + (erc--display-context `((erc-interactive-display . /QUERY) + ,@erc--display-context))) (erc-with-server-buffer (erc--open-target user)))) @@ -3916,10 +5130,7 @@ the message given by REASON." ;; kill them (run-at-time 4 nil - (lambda () - (dolist (buffer (erc-buffer-list (lambda (buf) - (not (erc-server-buffer-p buf))))) - (kill-buffer buffer))))) + #'erc-buffer-do (lambda () (when erc--target (kill-buffer))))) t) (defalias 'erc-cmd-GQ #'erc-cmd-GQUIT) @@ -3928,6 +5139,9 @@ the message given by REASON." (defun erc--cmd-reconnect () (let ((buffer (erc-server-buffer)) + (erc-join-buffer erc-interactive-display) + (erc--display-context `((erc-interactive-display . /RECONNECT) + ,@erc--display-context)) (process nil)) (unless (buffer-live-p buffer) (setq buffer (current-buffer))) @@ -3962,6 +5176,8 @@ connection or, with -A, all applicable connections. (put 'erc-cmd-RECONNECT 'process-not-needed t) +;; FIXME use less speculative error message or lose `condition-case', +;; since most connection failures don't signal anything. (defun erc-cmd-SERVER (server) "Connect to SERVER, leaving existing connection intact." (erc-log (format "cmd: SERVER: %s" server)) @@ -3980,9 +5196,11 @@ connection or, with -A, all applicable connections. system-configuration (concat (cond ((featurep 'motif) + (defvar motif-version-string) (concat ", " (substring motif-version-string 4))) ((featurep 'gtk) + (defvar gtk-version-string) (concat ", GTK+ Version " gtk-version-string)) ((featurep 'x-toolkit) ", X toolkit") @@ -4056,6 +5274,22 @@ means that the user has a +o flag in the channel's access list)." (t (erc-server-send "TIME")))) (defalias 'erc-cmd-DATE #'erc-cmd-TIME) +(defun erc-cmd-MOTD (&optional target) + "Ask server to send the current MOTD. +Some IRCds simply ignore TARGET." + (letrec ((oneoff (lambda (proc parsed) + (with-current-buffer (erc-server-buffer) + (cl-assert (eq (current-buffer) (process-buffer proc))) + (remove-hook 'erc-server-402-functions h402 t) + (remove-hook 'erc-server-376-functions h376 t) + (remove-hook 'erc-server-422-functions h422 t)) + (erc-server-MOTD proc parsed) + t)) + (h402 (erc-once-with-server-event 402 oneoff)) + (h376 (erc-once-with-server-event 376 oneoff)) + (h422 (erc-once-with-server-event 422 oneoff))) + (erc-server-send (concat "MOTD" (and target " ") target)))) + (defun erc-cmd-TOPIC (topic) "Set or request the topic for a channel. LINE has the format: \"#CHANNEL TOPIC\", \"#CHANNEL\", \"TOPIC\" @@ -4092,7 +5326,7 @@ be displayed." (progn (erc-log (format "cmd: TOPIC [%s]: %s" ch topic)) (erc-server-send (format "TOPIC %s :%s" ch topic) nil ch)) - (erc-display-message nil 'error (current-buffer) 'no-target))) + (erc-display-message nil '(notice error) (current-buffer) 'no-target))) t) (t nil))) (defalias 'erc-cmd-T #'erc-cmd-TOPIC) @@ -4142,8 +5376,7 @@ The ban list is fetched from the server if necessary." (cond ((not (erc-channel-p chnl)) - (erc-display-line (erc-make-notice "You're not on a channel\n") - 'active)) + (erc-display-message nil 'notice 'active "You're not on a channel\n")) ((not (get 'erc-channel-banlist 'received-from-server)) (let ((old-367-hook erc-server-367-functions)) @@ -4162,9 +5395,8 @@ The ban list is fetched from the server if necessary." (erc-server-send (format "MODE %s b" chnl))))) ((null erc-channel-banlist) - (erc-display-line (erc-make-notice - (format "No bans for channel: %s\n" chnl)) - 'active) + (erc-display-message nil 'notice 'active + (format "No bans for channel: %s\n" chnl)) (put 'erc-channel-banlist 'received-from-server nil)) (t @@ -4178,10 +5410,9 @@ The ban list is fetched from the server if necessary." "%-" (number-to-string (/ erc-fill-column 2)) "s" "%" (number-to-string (/ erc-fill-column 2)) "s"))) - (erc-display-line - (erc-make-notice (format "Ban list for channel: %s\n" - (erc-default-target))) - 'active) + (erc-display-message + nil 'notice 'active + (format "Ban list for channel: %s\n" (erc-default-target))) (erc-display-line separator 'active) (erc-display-line (format fmt "Ban Mask" "Banned By") 'active) @@ -4198,8 +5429,7 @@ The ban list is fetched from the server if necessary." 'active)) erc-channel-banlist) - (erc-display-line (erc-make-notice "End of Ban list") - 'active) + (erc-display-message nil 'notice 'active "End of Ban list") (put 'erc-channel-banlist 'received-from-server nil))))) t) @@ -4213,9 +5443,7 @@ Unban all currently banned users in the current channel." (cond ((not (erc-channel-p chnl)) - (erc-display-line - (erc-make-notice "You're not on a channel\n") - 'active)) + (erc-display-message nil 'notice 'active "You're not on a channel\n")) ((not (get 'erc-channel-banlist 'received-from-server)) (let ((old-367-hook erc-server-367-functions)) @@ -4256,6 +5484,32 @@ Eventually add a # in front of it, if that turns it into a valid channel name." channel (concat "#" channel))) +(defvar erc--own-property-names + `( tags erc--speaker erc-parsed display ; core + ;; `erc--msg-props' + ,@erc--ranked-properties + ;; `erc-display-prompt' + rear-nonsticky erc-prompt field front-sticky read-only + ;; stamp + cursor-intangible cursor-sensor-functions isearch-open-invisible + ;; match + invisible intangible + ;; button + erc-callback erc-data mouse-face keymap + ;; fill-wrap + line-prefix wrap-prefix) + "Props added by ERC that should not survive killing. +Among those left behind by default are `font-lock-face' and +`erc-secret'.") + +(defun erc--remove-text-properties (string) + "Remove text properties in STRING added by ERC. +Specifically, remove any that aren't members of +`erc--own-property-names'." + (remove-list-of-text-properties 0 (length string) + erc--own-property-names string) + string) + (defun erc-grab-region (start end) "Copy the region between START and END in a recreatable format. @@ -4304,12 +5558,7 @@ If FACE is non-nil, it will be used to propertize the prompt. If it is nil, ;; Do not extend the text properties when typing at the end ;; of the prompt, but stuff typed in front of the prompt ;; shall remain part of the prompt. - (setq prompt (propertize prompt - 'rear-nonsticky t - 'erc-prompt t - 'field t - 'front-sticky t - 'read-only t)) + (setq prompt (apply #'propertize prompt erc--prompt-properties)) (erc-put-text-property 0 (1- (length prompt)) 'font-lock-face (or face 'erc-prompt-face) prompt) @@ -4443,6 +5692,19 @@ This places `point' just after the prompt, or at the beginning of the line." (setq erc-input-ring-index nil)) (kill-line))) +(defvar erc--tab-functions nil + "Functions to try when user hits \\`TAB' outside of input area. +Called with a numeric prefix arg.") + +(defun erc-tab (arg) + "Call `completion-at-point' when typing in the input area. +Otherwise call members of `erc--tab-functions' with a numeric +prefix ARG until one of them returns non-nil." + (interactive "p") + (if (>= (point) erc-input-marker) + (completion-at-point) + (run-hook-with-args-until-success 'erc--tab-functions arg))) + (defun erc-complete-word-at-point () (run-hook-with-args-until-success 'erc-complete-functions)) @@ -4457,9 +5719,13 @@ This places `point' just after the prompt, or at the beginning of the line." ; Stolen from ZenIRC. I just wanna test this code, so here is ; experiment area. -(defcustom erc-default-server-hook '(erc-debug-missing-hooks - erc-default-server-handler) - "Default for server messages which aren't covered by `erc-server-hooks'." +;; This shouldn't be a user option but remains so for compatibility. +(define-obsolete-variable-alias + 'erc-default-server-hook 'erc-default-server-functions "30.1") +(defcustom erc-default-server-functions '(erc-handle-unknown-server-response) + "Abnormal hook for incoming messages without their own handlers. +See `define-erc-response-handler' for more." + :package-version '(ERC . "5.6") :group 'erc-server-hooks :type 'hook) @@ -4467,6 +5733,7 @@ This places `point' just after the prompt, or at the beginning of the line." "Default server handler. Displays PROC and PARSED appropriately using `erc-display-message'." + (declare (obsolete erc-handle-unknown-server-response "29.1")) (erc-display-message parsed 'notice proc (mapconcat @@ -4489,7 +5756,7 @@ See `erc-debug-missing-hooks'.") "Add PARSED server message ERC does not yet handle to `erc-server-vectors'. These vectors can be helpful when adding new server message handlers to ERC. See `erc-default-server-hook'." - (nconc erc-server-vectors (list parsed)) + (setq erc-server-vectors `(,@erc-server-vectors ,parsed)) nil) (defun erc--open-target (target) @@ -4517,47 +5784,60 @@ To change how this query window is displayed, use `let' to bind (with-current-buffer server-buffer (erc--open-target target))) -(defcustom erc-auto-query 'window-noselect +(defvaralias 'erc-auto-query 'erc-receive-query-display) +(defcustom erc-receive-query-display 'window-noselect "If non-nil, create a query buffer each time you receive a private message. If the buffer doesn't already exist, it is created. This can be set to a symbol, to control how the new query window should appear. The default behavior is to display the buffer in -a new window, but not to select it. See the documentation for -`erc-join-buffer' for a description of the available choices." +a new window but not to select it. See the documentation for +`erc-buffer-display' for a description of available values. + +Note that the legacy behavior of forgoing buffer creation +entirely when this option is nil requires setting the +compatibility flag `erc-receive-query-display-defer' to nil. Use +`erc-ensure-target-buffer-on-privmsg' to achieve the same effect." + :package-version '(ERC . "5.6") + :group 'erc-buffers :group 'erc-query - :type '(choice (const :tag "Don't create query window" nil) - (const :tag "Split window and select" window) - (const :tag "Split window, don't select" window-noselect) - (const :tag "New frame" frame) - (const :tag "Bury in new buffer" bury) - (const :tag "Use current buffer" buffer) - (const :tag "Use current buffer" t))) - -(defcustom erc-query-on-unjoined-chan-privmsg t - "If non-nil create query buffer on receiving any PRIVMSG at all. + :type erc--buffer-display-choices) + +(defvar erc-receive-query-display-defer t + "How to interpret a null `erc-receive-query-display'. +When this variable is non-nil, ERC defers to `erc-buffer-display' +upon seeing a nil value for `erc-receive-query-display', much +like it does with other buffer-display options, like +`erc-interactive-display'. Otherwise, when this option is nil, +ERC retains the legacy behavior of not creating a new query +buffer.") + +(defvaralias 'erc-query-on-unjoined-chan-privmsg + 'erc-ensure-target-buffer-on-privmsg) +(defcustom erc-ensure-target-buffer-on-privmsg t + "When non-nil, create a target buffer upon receiving a PRIVMSG. This includes PRIVMSGs directed to channels. If you are using an IRC bouncer, such as dircproxy, to keep a log of channels when you are disconnected, you should set this option to t. -WARNING: this option was mistakenly removed from ERC 5.5's client -code, so setting it to nil is temporarily ineffective. That is, -ERC now always creates a buffer when receiving a PRIVMSG directed -at a channel for which none exists. And despite this option's -name and its doc string's use of \"query\" to refer to any -conversation with a target, it did not previously allow for -opting out of buffer creation for direct messages (at least not -in Emacs 27 and 28). However, such behavior has always been and -will continue to be available by setting `erc-auto-query' to nil. -If needing to restore pre-5.5 functionality immediately, see Info -node `(erc) Upgrading'." +For queries (direct messages), this option's non-nil meaning is +straightforward: if a buffer doesn't exist for the sender, create +one. For channels, the use case is more niche and usually +involves receiving playback (via commands like ZNC's +\"PLAYBUFFER\") for channels to which your bouncer is joined but +from which you've \"detached\". + +Note that this option was absent from ERC 5.5 because knowledge +of its intended role was \"unavailable\" during a major +refactoring involving buffer management. The option has since +been restored in ERC 5.6 but now also affects queries in the +manner implied above, which was lost sometime before ERC 5.4." + :package-version '(ERC . "5.6") ; revived + :group 'erc-buffers :group 'erc-query - :set (lambda (sym val) - (unless (set sym val) - (lwarn 'erc :warning - "Setting `%s' to nil is currently ineffective; %s" - sym "see doc string for details."))) - :type 'boolean) + :type '(choice boolean + (choice :tag "Create pseudo queries for STATUSMSGs" + status))) (defcustom erc-format-query-as-channel-p t "If non-nil, format text from others in a query buffer like in a channel. @@ -4596,6 +5876,10 @@ E.g. \"Read error to Nick [user@some.host]: 110\" would be shortened to (match-string 1 reason)) reason)) +(cl-defmethod erc--nickname-in-use-make-request (_nick temp) + "Request nickname TEMP in place of rejected NICK." + (erc-cmd-NICK temp)) + (defun erc-nickname-in-use (nick reason) "If NICK is unavailable, tell the user the REASON. @@ -4629,7 +5913,7 @@ See also `erc-display-error-notice'." ;; established a connection yet (- 9 (length erc-nick-uniquifier)))) erc-nick-uniquifier))) - (erc-cmd-NICK newnick) + (erc--nickname-in-use-make-request nick newnick) (erc-display-error-notice nil (format "Nickname %s is %s, trying %s" @@ -4637,6 +5921,9 @@ See also `erc-display-error-notice'." ;;; Server messages +;; FIXME remove on next major version release. This group is all but +;; unused because most `erc-server-FOO-functions' are plain variables +;; and not user options as implied by this doc string. (defgroup erc-server-hooks nil "Server event callbacks. Every server event - like numeric replies - has its own hook. @@ -4684,57 +5971,318 @@ and as second argument the event parsed as a vector." (and (erc-is-message-ctcp-p message) (not (string-match "^\C-aACTION.*\C-a$" message)))) +(defun erc--get-speaker-bounds () + "Return the bounds of `erc--speaker' text property when present. +Assume buffer is narrowed to the confines of an inserted message." + (and-let* (((erc--check-msg-prop 'erc--spkr)) + (beg (text-property-not-all (point-min) (point-max) + 'erc--speaker nil))) + (cons beg (next-single-property-change beg 'erc--speaker)))) + +(defvar erc--cmem-from-nick-function #'erc--cmem-get-existing + "Function maybe returning a \"channel member\" cons from a nick. +Must return either nil or a cons of an `erc-server-user' and an +`erc-channel-user' (see `erc-channel-users') for use in +formatting a user's nick prior to insertion. Called in the +appropriate target buffer with the downcased nick, the parsed +NUH, and the current `erc-response' object.") + +(defun erc--cmem-get-existing (downcased _nuh _parsed) + (and erc-channel-users (gethash downcased erc-channel-users))) + (defun erc-format-privmessage (nick msg privp msgp) "Format a PRIVMSG in an insertable fashion." (let* ((mark-s (if msgp (if privp "*" "<") "-")) (mark-e (if msgp (if privp "*" ">") "-")) (str (format "%s%s%s %s" mark-s nick mark-e msg)) (nick-face (if privp 'erc-nick-msg-face 'erc-nick-default-face)) + (nick-prefix-face (get-text-property 0 'font-lock-face nick)) + (prefix-len (or (and nick-prefix-face (text-property-not-all + 0 (length nick) 'font-lock-face + nick-prefix-face nick)) + 0)) (msg-face (if privp 'erc-direct-msg-face 'erc-default-face))) + (erc--ensure-spkr-prop nick) ;; add text properties to text before the nick, the nick and after the nick (erc-put-text-property 0 (length mark-s) 'font-lock-face msg-face str) - (erc-put-text-property (length mark-s) (+ (length mark-s) (length nick)) - 'font-lock-face nick-face str) + (erc-put-text-properties (+ (length mark-s) prefix-len) + (+ (length mark-s) (length nick)) + '(font-lock-face erc--speaker) str + (list nick-face + (substring-no-properties nick prefix-len))) (erc-put-text-property (+ (length mark-s) (length nick)) (length str) 'font-lock-face msg-face str) str)) -(defcustom erc-format-nick-function 'erc-format-nick - "Function to format a nickname for message display." +;; The format strings in the following `-speaker' catalog shouldn't +;; contain any non-protocol words, so they make sense in any language. + +(defvar erc--message-speaker-statusmsg + #("(%p%n%s) %m" + 0 1 (font-lock-face erc-default-face) + 1 3 (font-lock-face erc-nick-prefix-face) + 3 5 (font-lock-face erc-nick-default-face) + 5 7 (font-lock-face erc-notice-face) + 7 11 (font-lock-face erc-default-face)) + "Message template for in-channel status messages.") + +(defvar erc--message-speaker-statusmsg-input + #("(%p%n%s) %m" + 0 1 (font-lock-face erc-default-face) + 1 3 (font-lock-face erc-my-nick-prefix-face) + 3 5 (font-lock-face erc-my-nick-face) + 5 7 (font-lock-face erc-notice-face) + 7 8 (font-lock-face erc-default-face) + 8 11 (font-lock-face erc-input-face)) + "Message template for echoed status messages.") + +(defvar erc--message-speaker-input-chan-privmsg + #("<%p%n> %m" + 0 1 (font-lock-face erc-default-face) + 1 3 (font-lock-face erc-my-nick-prefix-face) + 3 5 (font-lock-face erc-my-nick-face) + 5 7 (font-lock-face erc-default-face) + 7 9 (font-lock-face erc-input-face)) + "Message template for prompt input or echoed PRIVMSG from own nick.") + +(defvar erc--message-speaker-input-query-privmsg + #("*%n* %m" + 0 1 (font-lock-face erc-direct-msg-face) + 1 3 (font-lock-face erc-my-nick-face) + 3 5 (font-lock-face erc-direct-msg-face) + 5 7 (font-lock-face erc-input-face)) + "Message template for prompt input or echoed PRIVMSG query from own nick.") + +(defvar erc--message-speaker-input-query-notice + #("-%n- %m" + 0 1 (font-lock-face erc-direct-msg-face) + 1 3 (font-lock-face erc-my-nick-face) + 3 5 (font-lock-face erc-direct-msg-face) + 5 7 (font-lock-face erc-input-face)) + "Message template for echoed or spoofed query NOTICE from own nick.") + +(defvar erc--message-speaker-input-chan-notice + #("-%p%n- %m" + 0 1 (font-lock-face erc-default-face) + 1 3 (font-lock-face erc-my-nick-prefix-face) + 3 5 (font-lock-face erc-my-nick-face) + 5 7 (font-lock-face erc-default-face) + 7 9 (font-lock-face erc-input-face)) + "Message template for prompt input or echoed NOTICE from own nick.") + +(defvar erc--message-speaker-chan-privmsg + #("<%p%n> %m" + 0 1 (font-lock-face erc-default-face) + 1 3 (font-lock-face erc-nick-prefix-face) + 3 5 (font-lock-face erc-nick-default-face) + 5 9 (font-lock-face erc-default-face)) + "Message template for a PRIVMSG in a channel.") + +(defvar erc--message-speaker-query-privmsg + #("*%n* %m" + 0 1 (font-lock-face erc-direct-msg-face) + 1 3 (font-lock-face erc-nick-msg-face) + 3 7 (font-lock-face erc-direct-msg-face)) + "Message template for a PRIVMSG in query buffer.") + +(defvar erc--message-speaker-chan-notice + #("-%p%n- %m" + 0 1 (font-lock-face erc-default-face) + 1 3 (font-lock-face erc-nick-prefix-face) + 3 5 (font-lock-face erc-nick-default-face) + 5 9 (font-lock-face erc-default-face)) + "Message template for a NOTICE in a channel.") + +(defvar erc--message-speaker-query-notice + #("-%n- %m" + 0 1 (font-lock-face erc-direct-msg-face) + 1 3 (font-lock-face erc-nick-msg-face) + 3 7 (font-lock-face erc-direct-msg-face)) + "Message template for a NOTICE in a query buffer.") + +(defvar erc--message-speaker-ctcp-action + #("* %p%n %m" + 0 2 (font-lock-face erc-action-face) + 2 4 (font-lock-face (erc-nick-prefix-face erc-action-face)) + 4 9 (font-lock-face erc-action-face)) + "Message template for a CTCP ACTION from another user.") + +(defvar erc--message-speaker-ctcp-action-input + #("* %p%n %m" + 0 2 (font-lock-face #1=(erc-input-face erc-action-face)) + 2 4 (font-lock-face (erc-my-nick-prefix-face . #1#)) + 4 6 (font-lock-face (erc-my-nick-face . #1#)) + 6 9 (font-lock-face #1#)) + "Message template for a CTCP ACTION from current client.") + +(defvar erc--message-speaker-ctcp-action-statusmsg + #("* (%p%n%s) %m" + 0 3 (font-lock-face erc-action-face) + 3 5 (font-lock-face (erc-nick-prefix-face erc-action-face)) + 5 7 (font-lock-face erc-action-face) + 7 9 (font-lock-face (erc-notice-face erc-action-face)) + 9 13 (font-lock-face erc-action-face)) + "Template for a CTCP ACTION status message from another chan op.") + +(defvar erc--message-speaker-ctcp-action-statusmsg-input + #("* (%p%n%s) %m" + 0 3 (font-lock-face #1=(erc-input-face erc-action-face)) + 3 5 (font-lock-face (erc-my-nick-prefix-face . #1#)) + 5 7 (font-lock-face (erc-my-nick-face . #1#)) + 7 9 (font-lock-face (erc-notice-face . #1#)) + 9 13 (font-lock-face #1#)) + "Template for a CTCP ACTION status message from current client.") + +(defun erc--speakerize-nick (nick &optional disp) + "Propertize NICK with `erc--speaker' if not already present. +Do so to DISP instead if it's non-nil. In either case, assign +NICK, sans properties, as the `erc--speaker' value. As a side +effect, pair the latter string (the same `eq'-able object) with +the symbol `erc--spkr' in the \"msg prop\" environment for any +imminent `erc-display-message' invocations. While doing so, +include any overrides defined in `erc--message-speaker-catalog'." + (let ((plain-nick (substring-no-properties nick))) + (erc--ensure-spkr-prop plain-nick (get erc--message-speaker-catalog + 'erc--msg-prop-overrides)) + (if (text-property-not-all 0 (length (or disp nick)) + 'erc--speaker nil (or disp nick)) + (or disp nick) + (propertize (or disp nick) 'erc--speaker plain-nick)))) + +(defun erc--determine-speaker-message-format-args + (nick message queryp privmsgp inputp &optional statusmsg prefix disp-nick) + "Return a list consisting of a \"speaker\"-template key and spec args. +Consider the three flags QUERYP, PRIVMSGP, and INPUTP, as well as +the possibly null STATUSMSG string. (Combined, these describe +the context of a newly arrived \"PRIVMSG\" or, when PRIVMSGP is +nil, a \"NOTICE\"). Interpret QUERYP to mean that MESSAGE is +directed at the ERC client itself (a direct message), and INPUTP +to mean MESSAGE is an outgoing or echoed message originating from +or meant to simulate prompt input. Interpret a non-nil STATUSMSG +to mean MESSAGE should be formatted as a special channel message +intended for privileged members of the same or greater status. + +After deciding on the template key for the current \"speaker\" +catalog, use the remaining arguments, possibly along with +STATUSMSG, to construct the appropriate spec-args plist forming +the returned list's tail. In this plist, pair the char ?n with +NICK, the nickname of the speaker and ?m with MESSAGE, the +message body. When non-nil, assume DISP-NICK to be a possibly +phony display name to take the place of NICK for ?n. When PREFIX +is non-nil, look up NICK's channel-membership status, possibly +using PREFIX itself if it's an `erc-channel-user' object, which +it must be when called outside of a channel buffer. Pair the +result with the ?p specifier. When STATUSMSG is non-nil, pair it +with the ?s specifier. Ensure unused spec values are the empty +string rather than nil." + (when prefix + (setq prefix (erc-get-channel-membership-prefix + (if (erc-channel-user-p prefix) prefix nick)))) + (when (and queryp erc--target erc-format-query-as-channel-p + (not (erc--target-channel-p erc--target))) + (setq queryp nil)) + (list (cond (statusmsg (if inputp 'statusmsg-input 'statusmsg)) + (privmsgp (if queryp + (if inputp 'input-query-privmsg 'query-privmsg) + (if inputp 'input-chan-privmsg 'chan-privmsg))) + (t (if queryp + (if inputp 'input-query-notice 'query-notice) + (if inputp 'input-chan-notice 'chan-notice)))) + ?p (or prefix "") ?n (erc--speakerize-nick nick disp-nick) + ?s (or statusmsg "") ?m message)) + +(defcustom erc-show-speaker-membership-status nil + "Whether to prefix speakers with their channel status. +For example, when this option is non-nil and some nick \"Alice\" +has operator status in the current channel, ERC displays their +leading \"speaker\" label as <@Alice> instead of <Alice>." + :package-version '(ERC . "5.6") :group 'erc-display - :type 'function) + :type 'boolean) -(defun erc-format-nick (&optional user _channel-data) - "Return the nickname of USER. -See also `erc-format-nick-function'." - (when user (erc-server-user-nickname user))) +(define-obsolete-variable-alias 'erc-format-nick-function + 'erc-speaker-from-channel-member-function "30.1") +(defcustom erc-speaker-from-channel-member-function + #'erc-determine-speaker-from-user + "Function to determine a message's displayed \"speaker\" label. +Called with an `erc-server-user' object and an `erc-channel-user' +object, both possibly nil. Use this option to do things like +provide localized display names. To ask ERC to prepend +channel-membership \"status\" prefixes, like \"@\", to the +returned name, see `erc-show-speaker-membership-status'." + :package-version '(ERC . "5.6") + :group 'erc-display + :type '(choice (function-item erc-determine-speaker-from-user) function)) -(defun erc-get-user-mode-prefix (user) - (when user - (cond ((erc-channel-user-owner-p user) - (propertize "~" 'help-echo "owner")) - ((erc-channel-user-admin-p user) - (propertize "&" 'help-echo "admin")) - ((erc-channel-user-op-p user) - (propertize "@" 'help-echo "operator")) - ((erc-channel-user-halfop-p user) - (propertize "%" 'help-echo "half-op")) - ((erc-channel-user-voice-p user) - (propertize "+" 'help-echo "voice")) - (t "")))) +(define-obsolete-function-alias 'erc-format-nick + #'erc-determine-speaker-from-user "30.1") +(defun erc-determine-speaker-from-user (&optional user _channel-data) + "Return nickname slot of `erc-server-user' USER, when non-nil." + (when user (erc-server-user-nickname user))) -(defun erc-format-@nick (&optional user _channel-data) +(define-obsolete-function-alias 'erc-get-user-mode-prefix + #'erc-get-channel-membership-prefix "30.1") +(defun erc-get-channel-membership-prefix (nick-or-cusr) + "Return channel membership prefix for NICK-OR-CUSR as a string. +Ensure returned string has a `help-echo' text property with the +corresponding verbose membership type, like \"voice\", as its +value. Expect NICK-OR-CUSR to be an `erc-channel-user' object or +a string nickname, not necessarily downcased. When called in a +logically connected ERC buffer, use advertised prefix mappings. +For compatibility reasons, don't error when NICK-OR-CUSR is null, +but return nil instead of the empty string. Otherwise, always +return a possibly empty string." + (when nick-or-cusr + (when (stringp nick-or-cusr) + (setq nick-or-cusr (and erc-channel-members + (cdr (erc-get-channel-member nick-or-cusr))))) + (cond + ((null nick-or-cusr) "") + ;; Special-case most common value. + ((zerop (erc-channel-user-status nick-or-cusr)) "") + ;; For compatibility, first check whether a parsed prefix exists. + ((and-let* ((pfx-obj (erc--parsed-prefix))) + (catch 'done + (pcase-dolist (`(,letter . ,pfx) + (erc--parsed-prefix-alist pfx-obj)) + (when (erc--cusr-status-p nick-or-cusr letter) + (throw 'done + (pcase letter + (?q (propertize (string pfx) 'help-echo "owner")) + (?a (propertize (string pfx) 'help-echo "admin")) + (?o (propertize (string pfx) 'help-echo "operator")) + (?h (propertize (string pfx) 'help-echo "half-op")) + (?v (propertize (string pfx) 'help-echo "voice")) + (_ (string pfx)))))) + ""))) + (t + (cond ((erc-channel-user-owner nick-or-cusr) + (propertize "~" 'help-echo "owner")) + ((erc-channel-user-admin nick-or-cusr) + (propertize "&" 'help-echo "admin")) + ((erc-channel-user-op nick-or-cusr) + (propertize "@" 'help-echo "operator")) + ((erc-channel-user-halfop nick-or-cusr) + (propertize "%" 'help-echo "half-op")) + ((erc-channel-user-voice nick-or-cusr) + (propertize "+" 'help-echo "voice")) + (t "")))))) + +(defun erc-format-@nick (&optional user channel-data) "Format the nickname of USER showing if USER has a voice, is an operator, half-op, admin or owner. Owners have \"~\", admins have \"&\", operators have \"@\" and users with voice have \"+\" as a -prefix. Use CHANNEL-DATA to determine op and voice status. See -also `erc-format-nick-function'." +prefix. Use CHANNEL-DATA to determine op and voice status." + (declare (obsolete "see option `erc-show-speaker-membership-status'" "30.1")) (when user (let ((nick (erc-server-user-nickname user))) - (concat (propertize - (erc-get-user-mode-prefix nick) - 'font-lock-face 'erc-nick-prefix-face) - nick)))) + (if (not erc--speaker-status-prefix-wanted-p) + (prog1 nick + (setq erc--speaker-status-prefix-wanted-p 'erc-format-@nick)) + (concat (propertize + (erc-get-channel-membership-prefix channel-data) + 'font-lock-face 'erc-nick-prefix-face) + nick))))) (defun erc-format-my-nick () "Return the beginning of this user's message, correctly propertized." @@ -4742,15 +6290,42 @@ also `erc-format-nick-function'." (let* ((open "<") (close "> ") (nick (erc-current-nick)) - (mode (erc-get-user-mode-prefix nick))) + (mode (erc-get-channel-membership-prefix nick))) + (erc--ensure-spkr-prop nick) (concat (propertize open 'font-lock-face 'erc-default-face) (propertize mode 'font-lock-face 'erc-my-nick-prefix-face) - (propertize nick 'font-lock-face 'erc-my-nick-face) + (propertize nick 'erc--speaker nick 'font-lock-face 'erc-my-nick-face) (propertize close 'font-lock-face 'erc-default-face))) (let ((prefix "> ")) (propertize prefix 'font-lock-face 'erc-default-face)))) +(defun erc--format-speaker-input-message (message) + "Assemble outgoing MESSAGE entered at the prompt for insertion. +Intend \"input\" to refer to interactive prompt input as well as +the group of associated message-format templates from the +\"speaker\" catalog. Format the speaker portion in a manner +similar to that performed by `erc-format-my-nick', but use either +`erc--message-speaker-input-chan-privmsg' or +`erc--message-speaker-input-query-privmsg' as a formatting +template, with MESSAGE being the actual message body. Return a +copy with possibly shared text-property values." + (if-let ((erc-show-my-nick) + (nick (erc-current-nick)) + (pfx (erc-get-channel-membership-prefix nick)) + (erc-current-message-catalog erc--message-speaker-catalog) + (key (if (or erc-format-query-as-channel-p + (erc--target-channel-p erc--target)) + 'input-chan-privmsg + 'input-query-privmsg))) + (progn + (cond (erc--msg-props (puthash 'erc--msg key erc--msg-props)) + (erc--msg-prop-overrides (push (cons 'erc--msg key) + erc--msg-prop-overrides))) + (erc-format-message key ?p pfx ?n (erc--speakerize-nick nick) + ?m message)) + (propertize (concat "> " message) 'font-lock-face 'erc-input-face))) + (defun erc-echo-notice-in-default-buffer (s parsed buffer _sender) "Echo a private notice in the default buffer, namely the target buffer specified by BUFFER, or there is no target buffer, @@ -4921,9 +6496,7 @@ See also: `erc-echo-notice-in-user-buffers', (erc-load-script f))))) (defun erc-connection-established (proc parsed) - "Run just after connection. - -Set user modes and run `erc-after-connect' hook." + "Set user mode and run `erc-after-connect' hook in server buffer." (with-current-buffer (process-buffer proc) (unless erc-server-connected ; only once per session (let ((server (or erc-server-announced-name @@ -4933,26 +6506,39 @@ Set user modes and run `erc-after-connect' hook." (setq erc-server-connected t) (setq erc--server-last-reconnect-count erc-server-reconnect-count erc-server-reconnect-count 0) + (setq erc--server-reconnect-display-timer + (run-at-time erc-auto-reconnect-display-timeout nil + #'erc--server-last-reconnect-display-reset + (current-buffer))) + (add-hook 'erc-disconnected-hook + #'erc--server-last-reconnect-on-disconnect nil t) (erc-update-mode-line) (erc-set-initial-user-mode nick buffer) (erc-server-setup-periodical-ping buffer) - (run-hook-with-args 'erc-after-connect server nick)))) - - (when erc-unhide-query-prompt - (erc-with-all-buffers-of-server proc - nil ; FIXME use `erc--target' after bug#48598 - (when (and (erc-default-target) - (not (erc-channel-p (car erc-default-recipients)))) - (erc--unhide-prompt))))) + (when erc-unhide-query-prompt + (erc-with-all-buffers-of-server erc-server-process nil + (when (and erc--target (not (erc--target-channel-p erc--target))) + (erc--unhide-prompt)))) + (run-hook-with-args 'erc-after-connect server nick))))) (defun erc-set-initial-user-mode (nick buffer) "If `erc-user-mode' is non-nil for NICK, set the user modes. The server buffer is given by BUFFER." (with-current-buffer buffer (when erc-user-mode - (let ((mode (if (functionp erc-user-mode) - (funcall erc-user-mode) - erc-user-mode))) + (let* ((mode (if (functionp erc-user-mode) + (funcall erc-user-mode) + erc-user-mode)) + (groups (erc--parse-user-modes mode (erc--user-modes) t)) + (superfluous (last groups 2)) + (redundant-want (car superfluous)) + (redundant-drop (cadr superfluous))) + (when redundant-want + (erc-display-message nil 'notice buffer 'user-mode-redundant-add + ?m (apply #'string redundant-want))) + (when redundant-drop + (erc-display-message nil 'notice buffer 'user-mode-redundant-drop + ?m (apply #'string redundant-drop))) (when (stringp mode) (erc-log (format "changing mode for %s to %s" nick mode)) (erc-server-send (format "MODE %s %s" nick mode))))))) @@ -4978,7 +6564,9 @@ See also `erc-display-message'." 'ctcp-empty ?n nick) (while queries (let* ((type (upcase (car (split-string (car queries))))) - (hook (intern-soft (concat "erc-ctcp-query-" type "-hook")))) + (hook (intern-soft (concat "erc-ctcp-query-" type "-hook"))) + (erc--msg-prop-overrides `((erc--ctcp . ,(intern type)) + ,@erc--msg-prop-overrides))) (if (and hook (boundp hook)) (if (string-equal type "ACTION") (run-hook-with-args-until-success @@ -5012,10 +6600,31 @@ See also `erc-display-message'." (let ((s (match-string 1 msg)) (buf (or (erc-get-buffer to proc) (erc-get-buffer nick proc) - (process-buffer proc)))) - (erc-display-message - parsed 'action buf - 'ACTION ?n nick ?u login ?h host ?a s)))) + (process-buffer proc))) + (selfp (erc-current-nick-p nick))) + (if erc--use-language-catalog-for-ctcp-action-p + (progn + (erc--ensure-spkr-prop nick) + (setq nick (propertize nick 'erc--speaker nick)) + (erc-display-message parsed (if selfp 'input 'action) buf + 'ACTION ?n nick ?u login ?h host ?a s)) + (let* ((obj (and (erc--ctcp-response-p parsed) parsed)) + (buffer (and obj (erc--ctcp-response-buffer obj))) + (stsmsg (and obj (erc--ctcp-response-statusmsg obj))) + (prefix (and obj (erc--ctcp-response-prefix obj))) + (dispnm (and obj (erc--ctcp-response-dispname obj))) + (erc-current-message-catalog erc--message-speaker-catalog)) + (erc-display-message + parsed nil (or buffer buf) + (if selfp + (if stsmsg 'ctcp-action-statusmsg-input 'ctcp-action-input) + (if stsmsg 'ctcp-action-statusmsg 'ctcp-action)) + ?s (or stsmsg to) + ?p (or (and (erc-channel-user-p prefix) + (erc-get-channel-membership-prefix prefix)) + "") + ?n (erc--speakerize-nick nick dispnm) + ?m s)))))) (defvar erc-ctcp-query-CLIENTINFO-hook '(erc-ctcp-query-CLIENTINFO)) @@ -5089,8 +6698,14 @@ See also `erc-display-message'." (defun erc-process-ctcp-reply (proc parsed nick login host msg) "Process MSG as a CTCP reply." (let* ((type (car (split-string msg))) - (hook (intern (concat "erc-ctcp-reply-" type "-hook")))) - (if (boundp hook) + (hook (intern-soft (concat "erc-ctcp-reply-" type "-hook"))) + ;; Help `erc-display-message' by ensuring subsequent + ;; insertions retain the necessary props. + (cmd (erc--get-eq-comparable-cmd (erc-response.command parsed))) + (erc--msg-prop-overrides `((erc--ctcp . ,(and hook (intern type))) + (erc--cmd . ,cmd) + ,@erc--msg-prop-overrides))) + (if (and hook (boundp hook)) (run-hook-with-args-until-success hook proc nick login host (car (erc-response.command-args parsed)) msg) @@ -5224,22 +6839,78 @@ See also `erc-channel-begin-receiving-names'." (defun erc-parse-prefix () "Return an alist of valid prefix character types and their representations. -Example: (operator) o => @, (voiced) v => +." - (let ((str (or (erc-with-server-buffer (erc--get-isupport-entry 'PREFIX t)) - ;; provide a sane default - "(qaohv)~&@%+")) - types chars) - (when (string-match "^(\\([^)]+\\))\\(.+\\)$" str) - (setq types (match-string 1 str) - chars (match-string 2 str)) - (let ((len (min (length types) (length chars))) - (i 0) - (alist nil)) - (while (< i len) - (setq alist (cons (cons (elt types i) (elt chars i)) - alist)) - (setq i (1+ i))) - alist)))) +For example, if the current ISUPPORT \"PREFIX\" is \"(ov)@+\", +return an alist `equal' to ((?v . ?+) (?o . ?@)). For historical +reasons, ensure the ordering of the returned alist is opposite +that of the advertised parameter." + (let* ((str (or (erc--get-isupport-entry 'PREFIX t) "(qaohv)~&@%+")) + (i 0) + (j (string-search ")" str)) + collected) + (when j + (while-let ((u (aref str (cl-incf i))) + ((not (= ?\) u)))) + (push (cons u (aref str (cl-incf j))) collected))) + collected)) + +(defvar-local erc--parsed-prefix nil + "Possibly stale `erc--parsed-prefix' struct instance for the server. +Use the \"getter\" function of the same name to obtain the current +value.") + +(defun erc--parsed-prefix () + "Return possibly cached `erc--parsed-prefix' object for the server. +Ensure the returned value describes the most recent \"PREFIX\" +parameter advertised by the current server, with the original +ordering intact. If no such parameter has yet arrived, return a +stand-in from the fallback value \"(qaohv)~&@%+\"." + (erc--with-isupport-data PREFIX erc--parsed-prefix + (let ((alist (erc-parse-prefix))) + (make-erc--parsed-prefix + :key key + :letters (apply #'string (map-keys alist)) + :statuses (apply #'string (map-values alist)) + :alist (nreverse alist))))) + +(defun erc--get-prefix-flag (char &optional parsed-prefix from-prefix-p) + "Return numeric rank for CHAR or nil if unknown. +For example, given letters \"qaohv\" return 1 for ?v, 2 for ?h, +and 4 for ?o, etc. If given, expect PARSED-PREFIX to be a +`erc--parsed-prefix' object. With FROM-PREFIX-P, expect CHAR to +be a prefix instead." + (and-let* ((obj (or parsed-prefix (erc--parsed-prefix))) + (pos (erc--strpos char (if from-prefix-p + (erc--parsed-prefix-statuses obj) + (erc--parsed-prefix-letters obj))))) + (ash 1 pos))) + +(defun erc--init-cusr-fallback-status (voice halfop op admin owner) + "Return channel-membership based on traditional status semantics. +Massage boolean switches VOICE, HALFOP, OP, ADMIN, and OWNER into +an internal numeric value suitable for the `status' slot of a new +`erc-channel-user' object." + (let ((pfx (erc--parsed-prefix))) + (+ (if voice (if pfx (or (erc--get-prefix-flag ?v pfx) 0) 1) 0) + (if halfop (if pfx (or (erc--get-prefix-flag ?h pfx) 0) 2) 0) + (if op (if pfx (or (erc--get-prefix-flag ?o pfx) 0) 4) 0) + (if admin (if pfx (or (erc--get-prefix-flag ?a pfx) 0) 8) 0) + (if owner (if pfx (or (erc--get-prefix-flag ?q pfx) 0) 16) 0)))) + +(defun erc--compute-cusr-fallback-status (current v h o a q) + "Return current channel membership after toggling V H O A Q as requested. +Assume `erc--parsed-prefix' is non-nil in the current buffer. +Expect status switches V, H, O, A, Q, when non-nil, to be the +symbol `on' or `off'. Return an internal numeric value suitable +for the `status' slot of an `erc-channel-user' object." + (let (on off) + (when v (push (or (erc--get-prefix-flag ?v) 0) (if (eq v 'on) on off))) + (when h (push (or (erc--get-prefix-flag ?h) 0) (if (eq h 'on) on off))) + (when o (push (or (erc--get-prefix-flag ?o) 0) (if (eq o 'on) on off))) + (when a (push (or (erc--get-prefix-flag ?a) 0) (if (eq a 'on) on off))) + (when q (push (or (erc--get-prefix-flag ?q) 0) (if (eq q 'on) on off))) + (when on (setq current (apply #'logior current on))) + (when off (setq current (apply #'logand current (mapcar #'lognot off))))) + current) (defcustom erc-channel-members-changed-hook nil "This hook is called every time the variable `channel-members' changes. @@ -5247,48 +6918,40 @@ The buffer where the change happened is current while this hook is called." :group 'erc-hooks :type 'hook) -(defun erc-channel-receive-names (names-string) - "This function is for internal use only. +(defun erc--partition-prefixed-names (name) + "From NAME, return a list of (STATUS NICK LOGIN HOST). +Expect NAME to be a prefixed name, like @bob." + (unless (string-empty-p name) + (let* ((status (erc--get-prefix-flag (aref name 0) nil 'from-prefix-p)) + (nick (if status (substring name 1) name))) + (unless (string-empty-p nick) + (list status nick nil nil))))) -Update `erc-channel-users' according to NAMES-STRING. -NAMES-STRING is a string listing some of the names on the -channel." - (let* ((prefix (erc-parse-prefix)) - (voice-ch (cdr (assq ?v prefix))) - (op-ch (cdr (assq ?o prefix))) - (hop-ch (cdr (assq ?h prefix))) - (adm-ch (cdr (assq ?a prefix))) - (own-ch (cdr (assq ?q prefix))) - (names (delete "" (split-string names-string))) - name op voice halfop admin owner) - (let ((erc-channel-members-changed-hook nil)) - (dolist (item names) - (let ((updatep t) - (ch (aref item 0))) - (setq name item op 'off voice 'off halfop 'off admin 'off owner 'off) - (if (rassq ch prefix) - (if (= (length item) 1) - (setq updatep nil) - (setq name (substring item 1)) - (setf (pcase ch - ((pred (eq voice-ch)) voice) - ((pred (eq hop-ch)) halfop) - ((pred (eq op-ch)) op) - ((pred (eq adm-ch)) admin) - ((pred (eq own-ch)) owner) - (_ (message "Unknown prefix char `%S'" ch) voice)) - 'on))) - (when updatep +(defun erc-channel-receive-names (names-string) + "Update `erc-channel-members' from NAMES-STRING. +Expect NAMES-STRING to resemble the trailing argument of a 353 +RPL_NAMREPLY. Call internal handlers for parsing individual +names, whose expected composition may differ depending on enabled +extensions." + (let ((names (delete "" (split-string names-string))) + (erc-channel-members-changed-hook nil)) + (dolist (name names) + (when-let ((args (erc--partition-prefixed-names name))) + (pcase-let* ((`(,status ,nick ,login ,host) args) + (cmem (erc-get-channel-user nick))) + (progn ;; If we didn't issue the NAMES request (consider two clients ;; talking to an IRC proxy), `erc-channel-begin-receiving-names' ;; will not have been called, so we have to do it here. (unless erc-channel-new-member-names (erc-channel-begin-receiving-names)) - (puthash (erc-downcase name) t - erc-channel-new-member-names) - (erc-update-current-channel-member - name name t voice halfop op admin owner))))) - (run-hooks 'erc-channel-members-changed-hook))) + (puthash (erc-downcase nick) t erc-channel-new-member-names) + (if cmem + (erc--update-current-channel-member cmem status nil + nick host login) + (erc--create-current-channel-member nick status nil + nick host login))))))) + (run-hooks 'erc-channel-members-changed-hook)) (defun erc-update-user-nick (nick &optional new-nick host login full-name info) @@ -5340,111 +7003,114 @@ which USER is a member, and t is returned." (run-hooks 'erc-channel-members-changed-hook)))))) changed)) -(defun erc-update-current-channel-member - (nick new-nick &optional add voice halfop op admin owner host login full-name info - update-message-time) - "Update the stored user information for the user with nickname NICK. -`erc-update-user' is called to handle changes to nickname, -HOST, LOGIN, FULL-NAME, and INFO. If VOICE HALFOP OP ADMIN or OWNER -are non-nil, they must be equal to either `on' or `off', in which -case the status of the user in the current channel is changed accordingly. -If UPDATE-MESSAGE-TIME is non-nil, the last-message-time of the user - in the current channel is set to (current-time). - -If ADD is non-nil, the user will be added with the specified -information if it is not already present in the user or channel -lists. - -If, and only if, changes are made, or the user is added, -`erc-channel-members-changed-hook' is run, and t is returned. - -See also: `erc-update-user' and `erc-update-channel-member'." - (let* (changed user-changed - (channel-data (erc-get-channel-user nick)) - (cuser (cdr channel-data)) - (user (if channel-data (car channel-data) - (erc-get-server-user nick)))) - (if cuser +(defun erc--create-current-channel-member + (nick status timep &optional new-nick host login full-name info) + "Add an `erc-channel-member' entry for NICK. +Create a new `erc-server-users' entry if necessary, and ensure +`erc-channel-members-changed-hook' runs exactly once, regardless. +Pass STATUS to the `erc-channel-user' constructor. With TIMEP, +assume NICK has just spoken, and initialize `last-message-time'. +Pass NEW-NICK, HOST, LOGIN, FULL-NAME, and INFO to +`erc-update-user' if a server user exists and otherwise to the +`erc-server-user' constructor." + (cl-assert (null (erc-get-channel-member nick))) + (let* ((user-changed-p nil) + (down (erc-downcase nick)) + (user (gethash down (erc-with-server-buffer erc-server-users)))) + (if user (progn - (erc-log (format "update-member: user = %S, cuser = %S" user cuser)) - (when (and voice - (not (eq (erc-channel-user-voice cuser) voice))) - (setq changed t) - (setf (erc-channel-user-voice cuser) - (cond ((eq voice 'on) t) - ((eq voice 'off) nil) - (t voice)))) - (when (and halfop - (not (eq (erc-channel-user-halfop cuser) halfop))) - (setq changed t) - (setf (erc-channel-user-halfop cuser) - (cond ((eq halfop 'on) t) - ((eq halfop 'off) nil) - (t halfop)))) - (when (and op - (not (eq (erc-channel-user-op cuser) op))) - (setq changed t) - (setf (erc-channel-user-op cuser) - (cond ((eq op 'on) t) - ((eq op 'off) nil) - (t op)))) - (when (and admin - (not (eq (erc-channel-user-admin cuser) admin))) - (setq changed t) - (setf (erc-channel-user-admin cuser) - (cond ((eq admin 'on) t) - ((eq admin 'off) nil) - (t admin)))) - (when (and owner - (not (eq (erc-channel-user-owner cuser) owner))) - (setq changed t) - (setf (erc-channel-user-owner cuser) - (cond ((eq owner 'on) t) - ((eq owner 'off) nil) - (t owner)))) - (when update-message-time - (setf (erc-channel-user-last-message-time cuser) (current-time))) - (setq user-changed - (erc-update-user user new-nick - host login full-name info))) - (when add - (if (null user) - (progn - (setq user (make-erc-server-user - :nickname nick - :host host - :full-name full-name - :login login - :info info - :buffers (list (current-buffer)))) - (erc-add-server-user nick user)) - (setf (erc-server-user-buffers user) - (cons (current-buffer) - (erc-server-user-buffers user)))) - (setq cuser (make-erc-channel-user - :voice (cond ((eq voice 'on) t) - ((eq voice 'off) nil) - (t voice)) - :halfop (cond ((eq halfop 'on) t) - ((eq halfop 'off) nil) - (t halfop)) - :op (cond ((eq op 'on) t) - ((eq op 'off) nil) - (t op)) - :admin (cond ((eq admin 'on) t) - ((eq admin 'off) nil) - (t admin)) - :owner (cond ((eq owner 'on) t) - ((eq owner 'off) nil) - (t owner)) - :last-message-time - (if update-message-time (current-time)))) - (puthash (erc-downcase nick) (cons user cuser) - erc-channel-users) - (setq changed t))) - (when (and changed (null user-changed)) + (cl-pushnew (current-buffer) (erc-server-user-buffers user)) + ;; Update *after* ^ so hook has chance to run. + (setf user-changed-p (erc-update-user user new-nick host login + full-name info))) + (erc-add-server-user nick + (setq user (make-erc-server-user + :nickname (or new-nick nick) + :host host + :full-name full-name + :login login + :info nil + :buffers (list (current-buffer)))))) + (let ((cusr (erc-channel-user--make + :status (or status 0) + :last-message-time (and timep + (erc-compat--current-lisp-time))))) + (puthash down (cons user cusr) erc-channel-users)) + ;; An existing `cusr' was changed or a new one was added, and + ;; `user' was not updated, though possibly just created (since + ;; `erc-update-user' runs this same hook in all a user's buffers). + (unless user-changed-p + (run-hooks 'erc-channel-members-changed-hook)) + t)) + +(defun erc--update-current-channel-member (cmem status timep &rest user-args) + "Update existing `erc-channel-member' entry. +Set the `status' slot of the entry's `erc-channel-user' side to +STATUS and, with TIMEP, update its `last-message-time'. When +actual changes are made, run `erc-channel-members-changed-hook', +and return non-nil." + (cl-assert cmem) + (let ((cusr (cdr cmem)) + (user (car cmem)) + cusr-changed-p user-changed-p) + (when (and status (/= status (erc-channel-user-status cusr))) + (setf (erc-channel-user-status cusr) status + cusr-changed-p t)) + (when timep + (setf (erc-channel-user-last-message-time cusr) + (erc-compat--current-lisp-time))) + ;; Ensure `erc-channel-members-changed-hook' runs on change. + (cl-assert (memq (current-buffer) (erc-server-user-buffers user))) + (setq user-changed-p (apply #'erc-update-user user user-args)) + ;; An existing `cusr' was changed or a new one was added, and + ;; `user' was not updated, though possibly just created (since + ;; `erc-update-user' runs this same hook in all a user's buffers). + (when (and cusr-changed-p (null user-changed-p)) (run-hooks 'erc-channel-members-changed-hook)) - (or changed user-changed add))) + (erc-log (format "update-member: user = %S, cusr = %S" user cusr)) + (or cusr-changed-p user-changed-p))) + +(defun erc-update-current-channel-member + (nick new-nick &optional addp voice halfop op admin owner host login + full-name info update-message-time) + "Update or create entry for NICK in current `erc-channel-members' table. +With ADDP, ensure an entry exists. When an entry does exist or +when ADDP is non-nil and an `erc-server-users' entry already +exists, call `erc-update-user' with NEW-NICK, HOST, LOGIN, +FULL-NAME, and INFO. Expect any non-nil membership +status switches among VOICE, HALFOP, OP, ADMIN, and OWNER to be +the symbol `on' or `off' when needing to influence a new or +existing `erc-channel-user' object's `status' slot. Likewise, +when UPDATE-MESSAGE-TIME is non-nil, update or initialize the +`last-message-time' slot to the current-time. If changes occur, +including creation, run `erc-channel-members-changed-hook'. +Return non-nil when meaningful changes, including creation, have +occurred. + +Without ADDP, do nothing unless a `erc-channel-members' entry +exists. When it doesn't, assume the sender is a non-joined +entity, like the server itself or a historical speaker, or assume +the prior buffer for the channel was killed without parting." +(let* ((cmem (erc-get-channel-member nick)) + (status (and (or voice halfop op admin owner) + (if cmem + (erc--compute-cusr-fallback-status + (erc-channel-user-status (cdr cmem)) + voice halfop op admin owner) + (erc--init-cusr-fallback-status + (and voice (eq voice 'on)) + (and halfop (eq halfop 'on)) + (and op (eq op 'on)) + (and admin (eq admin 'on)) + (and owner (eq owner 'on))))))) + (if cmem + (erc--update-current-channel-member cmem status update-message-time + new-nick host login + full-name info) + (when addp + (erc--create-current-channel-member nick status update-message-time + new-nick host login + full-name info))))) (defun erc-update-channel-member (channel nick new-nick &optional add voice halfop op admin owner host login @@ -5489,7 +7155,9 @@ TOPIC string to the current topic." (defun erc-set-modes (tgt mode-string) "Set the modes for the TGT provided as MODE-STRING." - (let* ((modes (erc-parse-modes mode-string)) + (declare (obsolete "see comment atop `erc--update-modes'" "30.1")) + (let* ((modes (with-suppressed-warnings ((obsolete erc-parse-modes)) + (erc-parse-modes mode-string))) (add-modes (nth 0 modes)) ;; list of triples: (mode-char 'on/'off argument) (arg-modes (nth 2 modes))) @@ -5535,6 +7203,7 @@ for modes without parameters to add and remove respectively. The arg-modes is a list of triples of the form: (MODE-CHAR ON/OFF ARGUMENT)." + (declare (obsolete "see comment atop `erc--update-modes'" "30.1")) (if (string-match "^\\s-*\\(\\S-+\\)\\(\\s-.*$\\|$\\)" mode-string) (let ((chars (mapcar #'char-to-string (match-string 1 mode-string))) ;; arguments in channel modes @@ -5579,8 +7248,10 @@ arg-modes is a list of triples of the form: "Update the mode information for TGT, provided as MODE-STRING. Optional arguments: NICK, HOST and LOGIN - the attributes of the person who changed the modes." + (declare (obsolete "see comment atop `erc--update-modes'" "30.1")) ;; FIXME: neither of nick, host, and login are used! - (let* ((modes (erc-parse-modes mode-string)) + (let* ((modes (with-suppressed-warnings ((obsolete erc-parse-modes)) + (erc-parse-modes mode-string))) (add-modes (nth 0 modes)) (remove-modes (nth 1 modes)) ;; list of triples: (mode-char 'on/'off argument) @@ -5629,9 +7300,255 @@ person who changed the modes." ;; nick modes - ignored at this point (t nil)))) +(defvar-local erc--channel-modes nil + "When non-nil, a hash table of current channel modes. +Keys are characters. Values are either a string, for types A-C, +or t, for type D.") + +(defvar-local erc--channel-mode-types nil + "Possibly stale `erc--channel-mode-types' instance for the server. +Use the getter of the same name to retrieve the current value.") + +(defvar-local erc--mode-line-mode-string nil + "Computed mode-line or header-line component for user/channel modes.") + +(defvar erc--mode-line-chanmodes-arg-len 10 + "Max length at which to truncate channel-mode args in header line.") + +(defun erc--channel-mode-types () + "Return variable `erc--channel-mode-types', possibly initializing it." + (erc--with-isupport-data CHANMODES erc--channel-mode-types + (let ((types (or key '(nil "Kk" "Ll" nil))) + (ct (make-char-table 'erc--channel-mode-types)) + (type ?a)) + (dolist (cs types) + (erc--doarray (c cs) + (aset ct c type)) + (cl-incf type)) + (make-erc--channel-mode-types :key key + :fallbackp (null key) + :table ct)))) + +(defun erc--process-channel-modes (string args &optional status-letters) + "Parse channel \"MODE\" changes and call unary letter handlers. +Update `erc-channel-modes' and `erc--channel-modes'. With +STATUS-LETTERS, also update channel membership prefixes. Expect +STRING to be the second argument from an incoming \"MODE\" +command and ARGS to be the remaining arguments, which should +complement relevant letters in STRING." + (cl-assert (erc--target-channel-p erc--target)) + (let* ((obj (erc--channel-mode-types)) + (table (erc--channel-mode-types-table obj)) + (fallbackp (erc--channel-mode-types-fallbackp obj)) + (+p t)) + (erc--doarray (c string) + (cond ((= ?+ c) (setq +p t)) + ((= ?- c) (setq +p nil)) + ((and status-letters (string-search (string c) status-letters)) + (erc--cusr-change-status (pop args) c +p)) + ((and-let* ((group (or (aref table c) (and fallbackp ?d)))) + (erc--handle-channel-mode group c +p + (and (/= group ?d) + (or (/= group ?c) +p) + (pop args))) + t)) + ((not fallbackp) + (erc-display-message nil '(notice error) (erc-server-buffer) + (format "Unknown channel mode: %S" c))))) + (setq erc-channel-modes (sort erc-channel-modes #'string<)) + (setq erc--mode-line-mode-string + (concat "+" (erc--channel-modes erc--mode-line-chanmodes-arg-len))) + (erc-update-mode-line (current-buffer)))) + +(defvar-local erc--user-modes nil + "Sorted list of current user \"MODE\" letters. +Analogous to `erc-channel-modes' but chars rather than strings.") + +(defun erc--user-modes (&optional as-type) + "Return user \"MODE\" letters in a form described by AS-TYPE. +When AS-TYPE is the symbol `strings' (plural), return a list of +strings. When it's `string' (singular), return the same list +concatenated into a single string. When AS-TYPE is nil, return a +list of chars." + (let ((modes (or erc--user-modes (erc-with-server-buffer erc--user-modes)))) + (pcase as-type + ('strings (mapcar #'char-to-string modes)) + ('string (apply #'string modes)) + (_ modes)))) + +(defun erc--channel-modes (&optional as-type sep) + "Return channel \"MODE\" settings in a form described by AS-TYPE. +When AS-TYPE is the symbol `strings' (plural), return letter keys +as a list of sorted string. When it's `string' (singular), +return keys as a single string. When it's a number N, return a +single string consisting of the concatenated and sorted keys +followed by a space and then their corresponding args, each +truncated to N chars max. ERC joins these args together with +SEP, which defaults to a single space. Otherwise, return a +sorted alist of letter and arg pairs. In all cases that include +values, respect `erc-show-channel-key-p' and optionally omit the +secret key associated with the letter k." + (and-let* ((modes erc--channel-modes) + (tobj (erc--channel-mode-types)) + (types (erc--channel-mode-types-table tobj))) + (let (out) + (maphash (lambda (k v) + (unless (eq ?a (aref types k)) + (push (cons k + (and (not (eq t v)) + (not (and (eq k ?k) + (not (bound-and-true-p + erc-show-channel-key-p)))) + v)) + out))) + modes) + (setq out (cl-sort out #'< :key #'car)) + (pcase as-type + ('strings (mapcar (lambda (o) (char-to-string (car o))) out)) + ('string (apply #'string (mapcar #'car out))) + ((and (pred natnump) c) + (let (keys vals) + (pcase-dolist (`(,k . ,v) out) + (when v + (push (if (> (length v) c) + (with-memoization + (gethash (list c k v) + (erc--channel-mode-types-shortargs tobj)) + (truncate-string-to-width v c 0 nil t)) + v) + vals)) + (push k keys)) + (concat (apply #'string (nreverse keys)) (and vals " ") + (string-join (nreverse vals) (or sep " "))))) + (_ out))))) + +(defun erc--parse-user-modes (string &optional current extrap) + "Return lists of chars from STRING to add to and drop from CURRENT. +Expect STRING to be a so-called \"modestring\", the second +parameter of a \"MODE\" command, here containing only valid +user-mode letters. Expect CURRENT to be a list of chars +resembling those found in `erc--user-modes'. With EXTRAP, return +two additional lists of chars: those that would be added were +they not already present in CURRENT and those that would be +dropped were they not already absent." + (let ((addp t) + ;; + redundant-add redundant-drop adding dropping) + (erc--doarray (c string) + (pcase c + (?+ (setq addp t)) + (?- (setq addp nil)) + (_ (push c (let ((hasp (and current (memq c current)))) + (if addp + (if hasp redundant-add adding) + (if hasp dropping redundant-drop))))))) + (if extrap + (list (nreverse adding) (nreverse dropping) + (nreverse redundant-add) (nreverse redundant-drop)) + (list (nreverse adding) (nreverse dropping))))) + +(defun erc--update-user-modes (string) + "Update `erc--user-modes' from \"MODE\" STRING. +Return its value, a list of characters sorted by character code." + (prog1 + (setq erc--user-modes + (pcase-let ((`(,adding ,dropping) + (erc--parse-user-modes string erc--user-modes))) + (sort (seq-difference (nconc erc--user-modes adding) dropping) + #'<))) + (setq erc--mode-line-mode-string + (concat "+" (erc--user-modes 'string))))) + +(defun erc--update-channel-modes (string &rest args) + "Update `erc-channel-modes' and call individual mode handlers. +Also update membership prefixes, as needed. Expect STRING to be +a \"modestring\" and ARGS to match mode-specific parameters." + (let ((status-letters (or (erc-with-server-buffer + (erc--parsed-prefix-letters + (erc--parsed-prefix))) + "qaovhbQAOVHB"))) + (erc--process-channel-modes string args status-letters))) + +;; XXX this comment is referenced elsewhere (grep before deleting). +;; +;; The function `erc-update-modes' was deprecated in ERC 5.6 with no +;; immediate public replacement. Third parties needing such a thing +;; are encouraged to write to emacs-erc@gnu.org with ideas for a +;; mode-handler API, possibly one incorporating letter-specific +;; handlers, like `erc--handle-channel-mode' (below), which only +;; handles mode types A-C. +(defun erc--update-modes (raw-args) + "Handle user or channel \"MODE\" update from server. +Expect RAW-ARGS be a list consisting of a \"modestring\" followed +by mode-specific arguments." + (if (and erc--target (erc--target-channel-p erc--target)) + (apply #'erc--update-channel-modes raw-args) + (erc--update-user-modes (car raw-args)))) + +(defun erc--init-channel-modes (channel raw-args) + "Set CHANNEL modes from RAW-ARGS. +Expect RAW-ARGS to be a \"modestring\" without any status-prefix +chars, followed by applicable arguments." + (erc-with-buffer (channel) + (erc--process-channel-modes (car raw-args) (cdr raw-args)))) + +(cl-defgeneric erc--handle-channel-mode (type letter state arg) + "Handle a STATE change for mode LETTER of TYPE with ARG. +Expect to be called in the affected target buffer. Expect TYPE +to be a character, like ?a, representing an advertised +\"CHANMODES\" group. Expect LETTER to also be a character, and +expect STATE to be a boolean and ARGUMENT either a string or nil." + (erc-log (format "Channel-mode %c (type %s, arg %S) %s" + letter type arg (if state 'enabled 'disabled)))) + +(cl-defmethod erc--handle-channel-mode :before (type c state arg) + "Record STATE change for mode letter C. +When STATE is non-nil, add or update C's mapping in +`erc--channel-modes', associating it with ARG if C takes a +parameter and t otherwise. When STATE is nil, forget the +mapping. For type A, add up update a permanent mapping for C, +associating it with an integer indicating a running total of +STATE changes since joining the channel. In most cases, this +won't match the number known to the server." + (unless erc--channel-modes + (cl-assert (erc--target-channel-p erc--target)) + (setq erc--channel-modes (make-hash-table))) + (if (= type ?a) + (cl-callf (lambda (s) (+ (or s 0) (if state +1 -1))) + (gethash c erc--channel-modes)) + (if state + (puthash c (or arg t) erc--channel-modes) + (remhash c erc--channel-modes)))) + +(cl-defmethod erc--handle-channel-mode :before ((_ (eql ?d)) c state _) + "Update `erc-channel-modes' for any character C of nullary type D. +Remember when STATE is non-nil and forget otherwise." + (setq erc-channel-modes + (if state + (cl-pushnew (char-to-string c) erc-channel-modes :test #'equal) + (delete (char-to-string c) erc-channel-modes)))) + +;; We could specialize on type C, but that may be too brittle. +(cl-defmethod erc--handle-channel-mode (_ (_ (eql ?l)) state arg) + "Update channel user limit, remembering ARG when STATE is non-nil." + (erc-update-channel-limit (erc--target-string erc--target) + (if state 'on 'off) + arg)) + +;; We could specialize on type B, but that may be too brittle. +(cl-defmethod erc--handle-channel-mode (_ (_ (eql ?k)) state arg) + "Update channel key, remembering ARG when state is non-nil." + ;; Mimic old parsing behavior in which an ARG of "*" was discarded + ;; even though `erc-update-channel-limit' checks STATE first. + (erc-update-channel-key (erc--target-string erc--target) + (if state 'on 'off) + (if (equal arg "*") nil arg))) + (defun erc-update-channel-limit (channel onoff n) - ;; FIXME: what does ONOFF actually do? -- Lawrence 2004-01-08 - "Update CHANNEL's user limit to N." + "Update CHANNEL's user limit to N. +Expect ONOFF to be `on' when the mode is being enabled and `off' +otherwise. And because this mode is of \"type C\", expect N to +be non-nil only when enabling." (if (or (not (eq onoff 'on)) (and (stringp n) (string-match "^[0-9]+$" n))) (erc-with-buffer @@ -5703,20 +7620,28 @@ OBJECT is modified without being copied first. You can redefine or `defadvice' this function in order to add EmacsSpeak support." - (put-text-property start end property value object)) + (if erc--merge-text-properties-p + (erc--merge-prop start end property value object) + (put-text-property start end property value object))) -(defun erc-list (thing) - "Return THING if THING is a list, or a list with THING as its element." - (if (listp thing) - thing - (list thing))) +(defalias 'erc-list 'ensure-list) + +(defconst erc--parse-user-regexp-pedantic + (rx bot (? (? (group (+ (not (any "!@\r\n"))))) "!") + (? (? (group (+ nonl))) "@") + (? (group (+ nonl))) eot)) + +(defconst erc--parse-user-regexp-legacy + "^\\([^!\n]*\\)!\\([^@\n]*\\)@\\(.*\\)$") + +(defvar erc--parse-user-regexp erc--parse-user-regexp-legacy) (defun erc-parse-user (string) "Parse STRING as a user specification (nick!login@host). Return a list of the three separate tokens." (cond - ((string-match "^\\([^!\n]*\\)!\\([^@\n]*\\)@\\(.*\\)$" string) + ((string-match erc--parse-user-regexp string) (list (match-string 1 string) (match-string 2 string) (match-string 3 string))) @@ -5728,6 +7653,26 @@ Return a list of the three separate tokens." (t (list string "" "")))) +(defun erc--parse-nuh (string) + "Match STRING against `erc--parse-user-regexp-pedantic'. +Return nil or matching groups representing nick, login, and host, +any of which may be nil. Expect STRING not to contain leading +prefix chars. Return an empty nick component to indicate further +processing is required based on context. Interpret a lone token +lacking delimiters or one with only a leading \"!\" as a host. + +See associated unit test for precise behavior." + (when (string-match erc--parse-user-regexp-pedantic string) + (list (match-string 1 string) + (match-string 2 string) + (match-string 3 string)))) + +(defun erc--shuffle-nuh-nickward (nick login host) + "Interpret results of `erc--parse-nuh', promoting loners to nicks." + (cond (nick (cl-assert (null login)) (list nick login host)) + ((and (null login) host) (list host nil nil)) + ((and login (null host)) (list login nil nil)))) + (defun erc-extract-nick (string) "Return the nick corresponding to a user specification STRING. @@ -5771,76 +7716,200 @@ If that function has never been called, the value is 0.") "Minimum time, in seconds, before sending new lines via IRC. If the value is a number, `erc-send-current-line' signals an error if its previous invocation was fewer than this many seconds ago. -This is useful so that if you accidentally enter large amounts of text -into the ERC buffer, that text is not sent to the IRC server. - If the value is nil, `erc-send-current-line' always considers any -submitted line to be intentional." +submitted line to be intentional. + +This option mainly prevents text accidentally entered into Emacs +from being sent to the server. Offending sources include +terminal multiplexers, desktop-automation scripts, and anything +capable of rapidly submitting successive lines of prompt input. +For example, if you could somehow manage to type \"one \\`RET' +two \\`RET' three \\`RET'\" at the prompt in less than +`erc-accidental-paste-threshold-seconds', ERC would send \"one\" +to the server, leave \"two\" at the prompt, and insert \"three\" +into an \"overflow\" buffer. See `erc-inhibit-multiline-input' +and `erc-warn-about-blank-lines' for suppression involving input +yanked from the clipboard or the kill ring, which is a related +but separate concern. + +Users of terminal multiplexers, in particular, should look into +support for \"bracketed pasting\", provided on the Emacs side by +libraries like `xterm' (and usually enabled by default). When +everything's working smoothly, Emacs transparently arranges for +pasted text to appear on the kill ring, regardless of any +read-only warnings you may encounter. And when point is in the +prompt area, ERC automatically yanks that text for previewing but +holds off on submitting it, for obvious reasons." :group 'erc :version "26.1" :type '(choice number (other :tag "disabled" nil))) (defvar erc--input-line-delim-regexp (rx (| (: (? ?\r) ?\n) ?\r))) -(defun erc--blank-in-multiline-input-p (lines) - "Detect whether LINES contains a blank line. -When `erc-send-whitespace-lines' is in effect, return nil if -LINES is multiline or the first line is non-empty. When -`erc-send-whitespace-lines' is nil, return non-nil when any line -is empty or consists of one or more spaces, tabs, or form-feeds." - (catch 'return - (let ((multilinep (cdr lines))) - (dolist (line lines) - (when (if erc-send-whitespace-lines - (and (string-empty-p line) (not multilinep)) - (string-match (rx bot (* (in " \t\f")) eot) line)) - (throw 'return t)))))) +(defvar erc-command-regexp "^/\\([A-Za-z']+\\)\\(\\s-+.*\\|\\s-*\\)$" + "Regular expression used for matching commands in ERC.") (defun erc--check-prompt-input-for-excess-lines (_ lines) "Return non-nil when trying to send too many LINES." (when erc-inhibit-multiline-input - ;; Assume `erc--discard-trailing-multiline-nulls' is set to run - (let ((reversed (seq-drop-while #'string-empty-p (reverse lines))) - (max (if (eq erc-inhibit-multiline-input t) + (let ((max (if (eq erc-inhibit-multiline-input t) 2 erc-inhibit-multiline-input)) (seen 0) - msg) - (while (and (pop reversed) (< (cl-incf seen) max))) + last msg) + (while (and lines (setq last (pop lines)) (< (cl-incf seen) max))) (when (= seen max) - (setq msg (format "(exceeded by %d)" (1+ (length reversed)))) + (push last lines) + (setq msg + (format "-- exceeded by %d (%d chars)" + (length lines) + (apply #'+ (mapcar #'length lines)))) (unless (and erc-ask-about-multiline-input (y-or-n-p (concat "Send input " msg "?"))) (concat "Too many lines " msg)))))) -(defun erc--check-prompt-input-for-multiline-blanks (_ lines) - "Return non-nil when multiline prompt input has blank LINES." - (when (erc--blank-in-multiline-input-p lines) +(defun erc--check-prompt-input-for-something (string _) + (when (string-empty-p string) (if erc-warn-about-blank-lines "Blank line - ignoring..." 'invalid))) +(defun erc--count-blank-lines (lines) + "Report on the number of whitespace-only and empty LINES. +Return a list of (BLANKS TO-PAD TO-STRIP). Expect caller to know +that BLANKS includes non-empty whitespace-only lines and that no +padding or stripping has yet occurred." + (let ((real 0) (total 0) (pad 0) (strip 0)) + (dolist (line lines) + (if (string-match (rx bot (* (in " \t\f")) eot) line) + (progn + (cl-incf total) + (if (zerop (match-end 0)) + (cl-incf strip) + (cl-incf pad strip) + (setq strip 0))) + (cl-incf real) + (unless (zerop strip) + (cl-incf pad strip) + (setq strip 0)))) + (when (and (zerop real) (not (zerop total)) (= total (+ pad strip))) + (cl-incf strip (1- pad)) + (setq pad 1)) + (list total pad strip))) + +(defvar erc--check-prompt-explanation nil + "List of strings to print if no validator returns non-nil.") + +(defun erc--check-prompt-input-for-multiline-blanks (_ lines) + "Return non-nil when multiline prompt input has blank LINES. +Consider newlines to be intervening delimiters, meaning the empty +\"logical\" line between a trailing newline and `eob' constitutes +a separate message." + (pcase-let ((`(,total ,pad ,strip)(erc--count-blank-lines lines))) + (cond ((zerop total) nil) + ((and erc-warn-about-blank-lines erc-send-whitespace-lines) + (let (msg args) + (unless (zerop strip) + (push "stripping (%d)" msg) + (push strip args)) + (unless (zerop pad) + (when msg + (push "and" msg)) + (push "padding (%d)" msg) + (push pad args)) + (when msg + (push "blank" msg) + (push (if (> (apply #'+ args) 1) "lines" "line") msg)) + (when msg + (setf msg (nreverse msg) + (car msg) (capitalize (car msg)))) + (when msg + (push (apply #'format (string-join msg " ") (nreverse args)) + erc--check-prompt-explanation) + nil))) + (erc-warn-about-blank-lines + (concat (if (= total 1) + (if (zerop strip) "Blank" "Trailing") + (if (= total strip) + (format "%d trailing" strip) + (format "%d blank" total))) + (and (> total 1) (/= total strip) (not (zerop strip)) + (format " (%d trailing)" strip)) + (if (= total 1) " line" " lines") + " detected (see `erc-send-whitespace-lines')")) + (erc-send-whitespace-lines nil) + (t 'invalid)))) + (defun erc--check-prompt-input-for-point-in-bounds (_ _) "Return non-nil when point is before prompt." (when (< (point) (erc-beg-of-input-line)) "Point is not in the input area")) +;; Originally, `erc-send-current-line' inhibited sends whenever a +;; server buffer was missing. In 2007, this was narrowed to +;; occurrences involving process-dependent commands. However, the +;; accompanying error message, which was identical to that emitted by +;; `erc-server-send', "ERC: No process running", was always inaccurate +;; because a server buffer can be alive and its process dead. (defun erc--check-prompt-input-for-running-process (string _) - "Return non-nil unless in an active ERC server buffer." - (unless (or (erc-server-buffer-live-p) - (erc-command-no-process-p string)) - "ERC: No process running")) + "Return non-nil if STRING is a slash command missing a process. +Also do so when the server buffer has been killed." + ;; Even if the server buffer has been killed, the user should still + ;; be able to /reconnect and recall previous commands. + (and (not (erc-command-no-process-p string)) + (or (and (not (erc-server-buffer-live-p)) "Server buffer missing") + (and (not (erc-server-process-alive)) "Process not running")))) + +(defun erc--check-prompt-input-for-multiline-command (line lines) + "Return non-nil when non-blank lines follow a command line." + (when (and (cdr lines) + (string-match erc-command-regexp line) + (seq-drop-while #'string-empty-p (reverse (cdr lines)))) + "Excess input after command line")) (defvar erc--check-prompt-input-functions '(erc--check-prompt-input-for-point-in-bounds + erc--check-prompt-input-for-something + erc--check-prompt-input-for-multiline-command erc--check-prompt-input-for-multiline-blanks erc--check-prompt-input-for-running-process erc--check-prompt-input-for-excess-lines) "Validators for user input typed at prompt. -Called with latest input string submitted by user and the list of -lines produced by splitting it. If any member function returns -non-nil, processing is abandoned and input is left untouched. -When the returned value is a string, pass it to `erc-error'.") +Called with two arguments: the current input submitted by the +user, as a string, along with the same input as a list of +strings. If any member function returns non-nil, ERC abandons +processing and leaves pending input untouched in the prompt area. +When the returned value is a string, ERC passes it to +`user-error'. Any other non-nil value tells ERC to abort +silently. If all members return nil, and the variable +`erc--check-prompt-explanation' is a nonempty list of strings, +ERC prints them as a single message joined by newlines.") + +(defun erc--run-input-validation-checks (state) + "Run input checkers from STATE, an `erc--input-split' object." + (let* ((erc--check-prompt-explanation nil) + (msg (run-hook-with-args-until-success + 'erc--check-prompt-input-functions + (erc--input-split-string state) + (erc--input-split-lines state)))) + (cond ((stringp msg) (user-error msg)) + (msg (push msg (erc--input-split-abortp state))) + (erc--check-prompt-explanation + (message "%s" (string-join (nreverse erc--check-prompt-explanation) + "\n")))))) + +(defun erc--inhibit-slash-cmd-insertion (state) + "Don't insert STATE object's message if it's a \"slash\" command." + (when (erc--input-split-cmdp state) + (setf (erc--input-split-insertp state) nil))) + +(defun erc--make-input-split (string) + (make-erc--input-split + :string string + :insertp erc-insert-this + :sendp erc-send-this + :lines (split-string string erc--input-line-delim-regexp) + :cmdp (string-match erc-command-regexp string))) (defun erc-send-current-line () "Parse current line and send it to IRC." @@ -5855,32 +7924,25 @@ When the returned value is a string, pass it to `erc-error'.") (eolp)) (expand-abbrev)) (widen) - (if-let* ((str (erc-user-input)) - (msg (run-hook-with-args-until-success - 'erc--check-prompt-input-functions str - (split-string str erc--input-line-delim-regexp)))) - (when (stringp msg) - (erc-error msg)) - (let ((inhibit-read-only t) - (old-buf (current-buffer))) + (let* ((str (erc-user-input)) + (state (erc--make-input-split str))) + (run-hook-with-args 'erc--input-review-functions state) + (when-let (((not (erc--input-split-abortp state))) + (inhibit-read-only t) + (erc--current-line-input-split state) + (old-buf (current-buffer))) (progn ; unprogn this during next major surgery (erc-set-active-buffer (current-buffer)) ;; Kill the input and the prompt - (delete-region (erc-beg-of-input-line) - (erc-end-of-input-line)) + (delete-region erc-input-marker (erc-end-of-input-line)) (unwind-protect - (erc-send-input str 'skip-ws-chk) + (erc--send-input-lines (erc--run-send-hooks state)) ;; Fix the buffer if the command didn't kill it (when (buffer-live-p old-buf) (with-current-buffer old-buf (save-restriction (widen) - (goto-char (point-max)) - (when (processp erc-server-process) - (set-marker (process-mark erc-server-process) (point))) - (set-marker erc-insert-marker (point)) (let ((buffer-modified (buffer-modified-p))) - (erc-display-prompt) (set-buffer-modified-p buffer-modified)))))) ;; Only when last hook has been run... @@ -5896,19 +7958,81 @@ When the returned value is a string, pass it to `erc-error'.") erc-input-marker (erc-end-of-input-line))) -(defvar erc-command-regexp "^/\\([A-Za-z']+\\)\\(\\s-+.*\\|\\s-*\\)$" - "Regular expression used for matching commands in ERC.") - (defun erc--discard-trailing-multiline-nulls (state) - "Ensure last line of STATE's string is non-null. -But only when `erc-send-whitespace-lines' is non-nil. STATE is -an `erc--input-split' object." - (when (and erc-send-whitespace-lines (erc--input-split-lines state)) + "Remove trailing empty lines from STATE, an `erc--input-split' object. +When all lines are empty, remove all but the first." + (when (erc--input-split-lines state) (let ((reversed (nreverse (erc--input-split-lines state)))) - (when (string-empty-p (car reversed)) - (pop reversed) - (setf (erc--input-split-cmdp state) nil)) - (nreverse (seq-drop-while #'string-empty-p reversed))))) + (while (and (cdr reversed) (string-empty-p (car reversed))) + (setq reversed (cdr reversed))) + (setf (erc--input-split-lines state) (nreverse reversed))))) + +(defun erc--split-lines (state) + "Partition non-command input into lines of protocol-compliant length." + ;; Prior to ERC 5.6, line splitting used to be predicated on + ;; `erc-flood-protect' being non-nil. + (unless (or (zerop erc-split-line-length) (erc--input-split-cmdp state)) + (setf (erc--input-split-lines state) + (mapcan #'erc--split-line (erc--input-split-lines state))))) + +(defun erc--run-send-hooks (lines-obj) + "Run send-related hooks that operate on the entire prompt input. +Sequester some of the back and forth involved in honoring old +interfaces, such as the reconstituting and re-splitting of +multiline input. Optionally readjust lines to protocol length +limits and pad empty ones, knowing full well that additional +processing may still corrupt messages before they reach the send +queue. Expect LINES-OBJ to be an `erc--input-split' object." + (progn ; FIXME remove `progn' after code review. + (with-suppressed-warnings ((lexical str) (obsolete erc-send-this)) + (defvar str) ; see note in string `erc-send-input'. + (let* ((str (string-join (erc--input-split-lines lines-obj) "\n")) + (erc-send-this (erc--input-split-sendp lines-obj)) + (erc-insert-this (erc--input-split-insertp lines-obj)) + (state (progn + ;; This may change `str' and `erc-*-this'. + (run-hook-with-args 'erc-send-pre-hook str) + (make-erc-input + :string str + :insertp erc-insert-this + :sendp erc-send-this + :substxt (erc--input-split-substxt lines-obj) + :refoldp (erc--input-split-refoldp lines-obj))))) + (run-hook-with-args 'erc-pre-send-functions state) + (setf (erc--input-split-sendp lines-obj) (erc-input-sendp state) + (erc--input-split-insertp lines-obj) (erc-input-insertp state) + (erc--input-split-substxt lines-obj) (erc-input-substxt state) + (erc--input-split-refoldp lines-obj) (erc-input-refoldp state) + ;; See note in test of same name re trailing newlines. + (erc--input-split-lines lines-obj) + (let ((lines (split-string (erc-input-string state) + erc--input-line-delim-regexp))) + (if erc--allow-empty-outgoing-lines-p + lines + (cl-nsubst " " "" lines :test #'equal)))) + (when (erc--input-split-refoldp lines-obj) + (erc--split-lines lines-obj))))) + (when (and (erc--input-split-cmdp lines-obj) + (cdr (erc--input-split-lines lines-obj))) + (user-error "Multiline command detected" )) + lines-obj) + +(defun erc--send-input-lines (lines-obj) + "Send lines in `erc--input-split-lines' object LINES-OBJ." + (when (erc--input-split-sendp lines-obj) + (let ((insertp (erc--input-split-insertp lines-obj)) + (substxt (erc--input-split-substxt lines-obj))) + (when (and insertp substxt) + (setq insertp nil) + (if (functionp substxt) + (apply substxt (erc--input-split-lines lines-obj)) + (erc-display-msg substxt))) + (dolist (line (erc--input-split-lines lines-obj)) + (when insertp + (erc-display-msg line)) + (erc-process-input-line (concat line "\n") + (null erc-flood-protect) + (not (erc--input-split-cmdp lines-obj))))))) (defun erc-send-input (input &optional skip-ws-chk) "Treat INPUT as typed in by the user. @@ -5940,23 +8064,22 @@ Return non-nil only if we actually send anything." :insertp erc-insert-this :sendp erc-send-this)) (run-hook-with-args 'erc-pre-send-functions state) - (setq state (make-erc--input-split - :string (erc-input-string state) - :insertp (erc-input-insertp state) - :sendp (erc-input-sendp state) - :lines (split-string (erc-input-string state) - erc--input-line-delim-regexp) - :cmdp (string-match erc-command-regexp - (erc-input-string state)))) - (run-hook-with-args 'erc--pre-send-split-functions state) (when (and (erc-input-sendp state) erc-send-this) - (let ((lines (erc--input-split-lines state))) - (if (and (erc--input-split-cmdp state) (not (cdr lines))) - (erc-process-input-line (concat (car lines) "\n") t nil) + (if-let* ((first (split-string (erc-input-string state) + erc--input-line-delim-regexp)) + (split (mapcan #'erc--split-line first)) + (lines (nreverse (seq-drop-while #'string-empty-p + (nreverse split)))) + ((string-match erc-command-regexp (car lines)))) + (progn + ;; Asking users what to do here might make more sense. + (cl-assert (not (cdr lines))) + ;; The `force' arg (here t) is ignored for command lines. + (erc-process-input-line (concat (car lines) "\n") t nil)) + (progn ; temporarily preserve indentation (dolist (line lines) - (dolist (line (or (and erc-flood-protect (erc-split-line line)) - (list line))) + (progn ; temporarily preserve indentation (when (erc-input-insertp state) (erc-display-msg line)) (erc-process-input-line (concat line "\n") @@ -5964,23 +8087,28 @@ Return non-nil only if we actually send anything." t))))) (defun erc-display-msg (line) - "Display LINE as a message of the user to the current target at point." + "Insert LINE into current buffer and run \"send\" hooks. +Treat LINE as input submitted interactively at the prompt, such +as outgoing chat messages and echoed slash commands." (when erc-insert-this - (let ((insert-position (point))) - (insert (erc-format-my-nick)) - (let ((beg (point))) - (insert line) - (erc-put-text-property beg (point) - 'font-lock-face 'erc-input-face)) - (insert "\n") - (when (processp erc-server-process) - (set-marker (process-mark erc-server-process) (point))) - (set-marker erc-insert-marker (point)) - (save-excursion + (save-excursion + (erc--assert-input-bounds) + (let ((insert-position (marker-position (goto-char erc-insert-marker))) + (erc--msg-props (or erc--msg-props + (let ((ovs (seq-filter + #'cdr erc--msg-prop-overrides))) + (map-into `((erc--msg . msg) ,@(reverse ovs)) + 'hash-table))))) + (insert (erc--format-speaker-input-message line) "\n") (save-restriction (narrow-to-region insert-position (point)) (run-hooks 'erc-send-modify-hook) - (run-hooks 'erc-send-post-hook)))))) + (run-hooks 'erc-send-post-hook) + (cl-assert (> (- (point-max) (point-min)) 1)) + (add-text-properties (point-min) (1+ (point-min)) + (erc--order-text-properties-from-hash + erc--msg-props))) + (erc--refresh-prompt))))) (defun erc-command-symbol (command) "Return the ERC command symbol for COMMAND if it exists and is bound." @@ -5988,9 +8116,18 @@ Return non-nil only if we actually send anything." (when (fboundp cmd) cmd))) (defun erc-extract-command-from-line (line) - "Extract command and args from the input LINE. -If no command was given, return nil. If command matches, return a -list of the form: (command args) where both elements are strings." + "Extract a \"slash command\" and its args from a prompt-input LINE. +If LINE doesn't start with a slash command, return nil. If it +does, meaning the pattern `erc-command-regexp' matches, return a +list of the form (COMMAND ARGS), where COMMAND is either a symbol +for a known handler function or `erc-cmd-default' if unknown. +When COMMAND has the symbol property `do-not-parse-args', return +a string in place of ARGS: that is, either LINE itself, when LINE +consists of only whitespace, or LINE stripped of any trailing +whitespace, including a final newline. When COMMAND lacks the +symbol property `do-not-parse-args', return a possibly empty list +of non-whitespace tokens. Do not perform any shell-style parsing +of quoted or escaped substrings." (when (string-match erc-command-regexp line) (let* ((cmd (erc-command-symbol (match-string 1 line))) ;; note: return is nil, we apply this simply for side effects @@ -6060,28 +8197,15 @@ See also `erc-downcase'." ;; default target handling (defun erc--current-buffer-joined-p () - "Return whether the current target buffer is joined." - ;; This may be a reliable means of detecting subscription status, - ;; but it's also roundabout and awkward. Perhaps it's worth - ;; discussing adding a joined slot to `erc--target' for this. - (cl-assert erc--target) + "Return non-nil if the current buffer is a channel and is joined." (and (erc--target-channel-p erc--target) - (erc-get-channel-user (erc-current-nick)) t)) - -;; While `erc-default-target' happens to return nil in channel buffers -;; you've parted or from which you've been kicked, using it to detect -;; whether a channel is currently joined may become unreliable in the -;; future. For now, third-party code can use -;; -;; (erc-get-channel-user (erc-current-nick)) -;; -;; A predicate may be provided eventually. For retrieving a target's -;; name regardless of subscription or connection status, new library -;; code should use `erc--default-target'. Third-party code should -;; continue to use `erc-default-target'. + (erc--target-channel-joined-p erc--target) + t)) (defun erc-default-target () - "Return the current default target (as a character string) or nil if none." + "Return the current channel or query target, if any. +For historical reasons, return nil in channel buffers if not +currently joined." (let ((tgt (car erc-default-recipients))) (cond ((not tgt) nil) @@ -6128,6 +8252,8 @@ The previous default target of QUERY type gets removed." (setq erc-default-recipients d2) (error "Current target is not a QUERY")))) +;; FIXME move all ignore-related functionality to its own module, +;; required and enabled by default (until some major version change). (defun erc-ignored-user-p (spec) "Return non-nil if SPEC matches something in `erc-ignore-list'. @@ -6388,7 +8514,8 @@ and so on." ((string-match "^%[Ss]$" esc) server) ((string-match "^%[Nn]$" esc) nick) ((string-match "^%\\(.\\)$" esc) (match-string 1 esc)) - (t (erc-log (format "BUG in erc-process-script-line: bad escape sequence: %S\n" esc)) + (t (erc-log (format "Bad escape sequence in %s: %S\n" + 'erc-process-script-line esc)) (message "BUG IN ERC: esc=%S" esc) ""))) (setq line tail) @@ -6407,34 +8534,6 @@ and so on." (buffer-string)))) (erc-load-irc-script-lines (erc-split-multiline-safe str) force))) -(defun erc-load-irc-script-lines (lines &optional force noexpand) - "Load IRC script LINES (a list of strings). - -If optional NOEXPAND is non-nil, do not expand script-specific -sequences, process the lines verbatim. Use this for multiline -user input." - (let* ((cb (current-buffer)) - (s "") - (sp (or (erc-command-indicator) (erc-prompt))) - (args (and (boundp 'erc-script-args) erc-script-args))) - (if (and args (string-match "^ " args)) - (setq args (substring args 1))) - ;; prepare the prompt string for echo - (erc-put-text-property 0 (length sp) - 'font-lock-face 'erc-command-indicator-face sp) - (while lines - (setq s (car lines)) - (erc-log (concat "erc-load-script: CMD: " s)) - (unless (string-match "^\\s-*$" s) - (let ((line (if noexpand s (erc-process-script-line s args)))) - (if (and (erc-process-input-line line force) - erc-script-echo) - (progn - (erc-put-text-property 0 (length line) - 'font-lock-face 'erc-input-face line) - (erc-display-line (concat sp line) cb))))) - (setq lines (cdr lines))))) - ;; authentication (defun erc--unfun (maybe-fn) @@ -6643,15 +8742,14 @@ If it doesn't exist, create it." (unless (file-attributes dir) (make-directory dir)) (or (file-accessible-directory-p dir) (error "Cannot access %s" dir))) +;; FIXME make function obsolete or alias to something less confusing. (defun erc-kill-query-buffers (process) - "Kill all buffers of PROCESS. -Does nothing if PROCESS is not a process object." + "Kill all target buffers of PROCESS, including channel buffers. +Do nothing if PROCESS is not a process object." ;; here, we only want to match the channel buffers, to avoid ;; "selecting killed buffers" b0rkage. (when (processp process) - (erc-with-all-buffers-of-server process - (lambda () - (not (erc-server-buffer-p))) + (erc-with-all-buffers-of-server process (lambda () erc--target) (kill-buffer (current-buffer))))) (defun erc-nick-at-point () @@ -6743,8 +8841,13 @@ See `erc-mode-line-format' for which characters are can be used." :type '(choice (const :tag "Disabled" nil) string)) +;; This should optionally support the built-in `tab-bar'. (defcustom erc-header-line-uses-tabbar-p nil - "Use tabbar mode instead of the header line to display the header." + "Use `tabbar-mode' integration instead of the header line. +This concerns a historical integration with the external library +`tabbar' <https://www.emacswiki.org/emacs/tabbar.el>, which +shouldn't be confused with the built-in `tab-bar' described in +Info node `(emacs) Tab Bars'." :group 'erc-mode-line-and-header :type 'boolean) @@ -6835,6 +8938,62 @@ shortened server name instead." (format-time-string erc-mode-line-away-status-format a) ""))) +(defvar-local erc--away-indicator nil + "Cons containing an away indicator for the connection.") + +(defvar erc-away-status-indicator "A" + "String shown by various formatting facilities to indicate away status. +Currently only used by the option `erc-prompt-format'.") + +(defun erc--format-away-indicator () + "Return char with `display' property of `erc--away-indicator'." + (and-let* ((indicator (erc-with-server-buffer + (or erc--away-indicator + (setq erc--away-indicator (list ""))))) + (newcar (if (erc-away-time) erc-away-status-indicator ""))) + ;; Inform other buffers of the change when necessary. + (let ((dispp (not erc--inhibit-prompt-display-property-p))) + (unless (eq newcar (car indicator)) + (erc--refresh-prompt-continue (and dispp 'hooks-only-p)) + (setcar indicator newcar)) + (if dispp + (propertize "(away?)" 'display indicator) + newcar)))) + +(defvar-local erc--user-modes-indicator nil + "Cons containing connection-wide indicator for user modes.") + +;; If adding more of these functions, should factor out commonalities. +;; As of ERC 5.6, this is identical to the away variant aside from +;; the var names and `eq', which isn't important. +(defun erc--format-user-modes () + "Return server's user modes as a string" + (and-let* ((indicator (erc-with-server-buffer + (or erc--user-modes-indicator + (setq erc--user-modes-indicator (list ""))))) + (newcar (erc--user-modes 'string))) + (let ((dispp (not erc--inhibit-prompt-display-property-p))) + (unless (string= newcar (car indicator)) + (erc--refresh-prompt-continue (and dispp 'hooks-only-p)) + (setcar indicator newcar)) + (if dispp + (propertize "(user-modes?)" 'display indicator) + newcar)))) + +(defun erc--format-channel-status-prefix () + "Return the current channel membership prefix." + (and (erc--target-channel-p erc--target) + (erc-get-channel-membership-prefix (erc-current-nick)))) + +(defun erc--format-modes (&optional no-query-p) + "Return a string of channel modes in channels and user modes elsewhere. +With NO-QUERY-P, return nil instead of user modes in query +buffers. Also return nil when mode information is unavailable." + (cond ((erc--target-channel-p erc--target) + (erc--channel-modes 'string)) + ((not (and erc--target no-query-p)) + (erc--format-user-modes)))) + (defun erc-format-channel-modes () "Return the current channel's modes." (concat (apply #'concat @@ -6859,8 +9018,6 @@ shortened server name instead." (cond (lag (format "lag:%.0f" lag)) (t "")))) -;; erc-goodies is required at end of this file. - ;; TODO when ERC drops Emacs 28, replace the expressions in the format ;; spec below with functions. (defun erc-update-mode-line-buffer (buffer) @@ -6868,7 +9025,7 @@ shortened server name instead." (with-current-buffer buffer (let ((spec `((?a . ,(erc-format-away-status)) (?l . ,(erc-format-lag-time)) - (?m . ,(erc-format-channel-modes)) + (?m . ,(or erc--mode-line-mode-string "")) (?n . ,(or (erc-current-nick) "")) (?N . ,(erc-format-network)) (?o . ,(or (erc-controls-strip erc-channel-topic) "")) @@ -6897,7 +9054,8 @@ shortened server name instead." (format-spec erc-header-line-format spec) nil))) (cond (erc-header-line-uses-tabbar-p - (setq-local tabbar--local-hlf header-line-format) + (when (boundp 'tabbar--local-hlf) + (setq-local tabbar--local-hlf header-line-format)) (kill-local-variable 'header-line-format)) ((null header) (setq header-line-format nil)) @@ -6940,6 +9098,54 @@ If BUFFER is nil, update the mode line in all ERC buffers." (goto-char (point-min)) (insert "X-Debbugs-CC: emacs-erc@gnu.org\n"))) +(defconst erc--news-url + "https://git.savannah.gnu.org/cgit/emacs.git/plain/etc/ERC-NEWS") + +(defvar erc--news-temp-file nil) + +(defun erc-news (arg) + "Show ERC news in a manner similar to `view-emacs-news'. +With ARG, download and display the latest revision, which may +contain more up-to-date information, even for older versions." + (interactive "P") + (find-file + (or (and erc--news-temp-file + (time-less-p (current-time) (car erc--news-temp-file)) + (not (and arg (y-or-n-p (format "Re-fetch? ")))) + (cdr erc--news-temp-file)) + (and arg + (with-current-buffer (url-retrieve-synchronously erc--news-url) + (goto-char (point-min)) + (search-forward "200 OK" (pos-eol)) + (search-forward "\n\n") + (delete-region (point-min) (point)) + ;; May warn about file having changed on disk (unless + ;; `query-about-changed-file' is nil on 28+). + (let ((tempfile (or (cdr erc--news-temp-file) + (make-temp-file "erc-news.")))) + (write-region (point-min) (point-max) tempfile) + (kill-buffer) + (cdr (setq erc--news-temp-file + (cons (time-add (current-time) (* 60 60 12)) + tempfile)))))) + (and-let* ((file (or (eval-when-compile (macroexp-file-name)) + (locate-library "erc"))) + (dir (file-name-directory file)) + (adjacent (expand-file-name "ERC-NEWS" dir)) + ((file-exists-p adjacent))) + adjacent) + (expand-file-name "ERC-NEWS" data-directory))) + (when (fboundp 'emacs-news-view-mode) + (emacs-news-view-mode)) + (goto-char (point-min)) + (let ((v (mapcar #'number-to-string + (seq-take-while #'natnump (version-to-list erc-version))))) + (while (and v (not (search-forward (concat "\014\n* Changes in ERC " + (string-join v ".")) + nil t))) + (setq v (butlast v)))) + (beginning-of-line)) + (defun erc-port-to-string (p) "Convert port P to a string. P may be an integer or a service name." @@ -6983,10 +9189,11 @@ If optional argument HERE is non-nil, insert version number at point." (let (modes (case-fold-search nil)) (dolist (var (apropos-internal "^erc-.*mode$")) (when (and (boundp var) + (get var 'erc-module) (symbol-value var)) - (setq modes (cons (symbol-name var) + (setq modes (cons (concat "`" (symbol-name var) "'") modes)))) - modes) + (sort modes #'string<)) ", "))) (if here (insert string) @@ -7042,24 +9249,38 @@ All windows are opened in the current frame." ;;; Message catalog +(define-inline erc--make-message-variable-name (catalog key softp) + "Return variable name conforming to ERC's message-catalog interface. +Given a CATALOG symbol `mycat' and format-string KEY `mykey', +also a symbol, return the symbol `erc-message-mycat-mykey'. With +SOFTP, only do so when defined as a variable." + (inline-quote + (let* ((catname (symbol-name ,catalog)) + (prefix (if (eq ?- (aref catname 0)) "erc--message" "erc-message-")) + (name (concat prefix catname "-" (symbol-name ,key)))) + (if ,softp + (and-let* ((s (intern-soft name)) ((boundp s))) s) + (intern name))))) + (defun erc-make-message-variable-name (catalog entry) "Create a variable name corresponding to CATALOG's ENTRY." - (intern (concat "erc-message-" - (symbol-name catalog) "-" (symbol-name entry)))) + (erc--make-message-variable-name catalog entry nil)) (defun erc-define-catalog-entry (catalog entry format-spec) "Set CATALOG's ENTRY to FORMAT-SPEC." + (declare (obsolete "define manually using `defvar' instead" "30.1")) (set (erc-make-message-variable-name catalog entry) format-spec)) (defun erc-define-catalog (catalog entries) "Define a CATALOG according to ENTRIES." - (dolist (entry entries) - (erc-define-catalog-entry catalog (car entry) (cdr entry)))) + (declare (obsolete erc-define-message-format-catalog "30.1")) + (with-suppressed-warnings ((obsolete erc-define-catalog-entry)) + (dolist (entry entries) + (erc-define-catalog-entry catalog (car entry) (cdr entry))))) -(erc-define-catalog - 'english - '((bad-ping-response . "Unexpected PING response from %n (time %t)") +(erc--define-catalog english + ((bad-ping-response . "Unexpected PING response from %n (time %t)") (bad-syntax . "Error occurred - incorrect usage?\n%c %u\n%d") (incorrect-args . "Incorrect arguments. Usage:\n%c %u\n%d") (cannot-find-file . "Cannot find file %f") @@ -7074,9 +9295,10 @@ All windows are opened in the current frame." (flood-ctcp-off . "FLOOD PROTECTION: Automatic CTCP responses turned off.") (flood-strict-mode . "FLOOD PROTECTION: Switched to Strict Flood Control mode.") - (disconnected . "\n\nConnection failed! Re-establishing connection...\n") + (disconnected + . "\n\n*** Connection failed! Re-establishing connection...\n") (disconnected-noreconnect - . "\n\nConnection failed! Not re-establishing connection.\n") + . "\n\n*** Connection failed! Not re-establishing connection.\n") (reconnecting . "Reconnecting in %ms: attempt %i/%n ...") (reconnect-canceled . "Canceled %u reconnect timer with %cs to go...") (finished . "\n\n*** ERC finished ***\n") @@ -7091,6 +9313,10 @@ All windows are opened in the current frame." (ops . "%i operator%s: %o") (ops-none . "No operators in this channel.") (undefined-ctcp . "Undefined CTCP query received. Silently ignored") + (user-mode-redundant-add + . "Already have user mode(s): %m. Requesting again anyway.") + (user-mode-redundant-drop + . "Already without user mode(s): %m. Requesting removal anyway.") (variable-not-bound . "Variable not bound!") (ACTION . "* %n %a") (CTCP-CLIENTINFO . "Client info for %n: %m") @@ -7111,7 +9337,7 @@ All windows are opened in the current frame." (MODE-nick . "%n has changed mode for %t to %m") (NICK . "%n (%u@%h) is now known as %N") (NICK-you . "Your new nickname is %N") - (PART . erc-message-english-PART) + (PART . #'erc-message-english-PART) (PING . "PING from server (last: %s sec. ago)") (PONG . "PONG from %h (%i second%s)") (QUIT . "%n (%u@%h) has quit: %r") @@ -7153,7 +9379,9 @@ All windows are opened in the current frame." (s368 . "Banlist of %c ends.") (s379 . "%c: Forwarded to %f") (s391 . "The time at %s is %t") + (s396 . "Your visible host has changed to %s") (s401 . "%n: No such nick/channel") + (s402 . "%c: No such server") (s403 . "%c: No such channel") (s404 . "%c: Cannot send to channel") (s405 . "%c: You have joined too many channels") @@ -7171,6 +9399,8 @@ All windows are opened in the current frame." (s463 . "Your host isn't among the privileged") (s464 . "Password incorrect") (s465 . "You are banned from this server") + (s471 . "Max occupancy for channel %c exceeded: %s") + (s473 . "Channel %c is invitation only") (s474 . "You can't join %c because you're banned (+b)") (s475 . "You must specify the correct channel key (+k) to join %c") (s481 . "Permission Denied - You're not an IRC operator") @@ -7202,22 +9432,26 @@ functions." (string-replace "%" "%%" reason)) ""))))) - -(defvar-local erc-current-message-catalog 'english) - -(defun erc-retrieve-catalog-entry (entry &optional catalog) - "Retrieve ENTRY from CATALOG. - -If CATALOG is nil, `erc-current-message-catalog' is used. - -If ENTRY is nil in CATALOG, it is retrieved from the fallback, -english, catalog." +(defun erc-retrieve-catalog-entry (key &optional catalog) + "Retrieve `format-spec' entry for symbol KEY in CATALOG. +Without symbol CATALOG, use `erc-current-message-catalog'. If +lookup fails, try the latter's `default-toplevel-value' if it's +not the same as CATALOG. Failing that, try the `english' catalog +if yet untried." (unless catalog (setq catalog erc-current-message-catalog)) - (let ((var (erc-make-message-variable-name catalog entry))) - (if (boundp var) - (symbol-value var) - (when (boundp (erc-make-message-variable-name 'english entry)) - (symbol-value (erc-make-message-variable-name 'english entry)))))) + (symbol-value + (or (erc--make-message-variable-name catalog key 'softp) + (let ((parent catalog) + last) + (while (and (setq parent (get parent 'erc--base-format-catalog)) + (not (setq last (erc--make-message-variable-name + parent key 'softp))))) + last) + (let ((default (default-toplevel-value 'erc-current-message-catalog))) + (or (and (not (eq default catalog)) + (erc--make-message-variable-name default key 'softp)) + (and (not (memq 'english (list default catalog))) + (erc--make-message-variable-name 'english key 'softp))))))) (defun erc-format-message (msg &rest args) "Format MSG according to ARGS. @@ -7260,6 +9494,7 @@ See also `kill-buffer'." :group 'erc-hooks :type 'hook) +;; FIXME alias and deprecate current *-function suffixed name. (defun erc-kill-buffer-function () "Function to call when an ERC buffer is killed. This function should be on `kill-buffer-hook'. @@ -7273,11 +9508,28 @@ or `erc-kill-buffer-hook' if any other buffer." (cond ((eq (erc-server-buffer) (current-buffer)) (run-hooks 'erc-kill-server-hook)) - ((erc-channel-p (or (erc-default-target) (buffer-name))) + ((erc--target-channel-p erc--target) (run-hooks 'erc-kill-channel-hook)) (t (run-hooks 'erc-kill-buffer-hook))))) +(declare-function set-text-conversion-style "textconv.c") + +(defun erc-check-text-conversion () + "Check if point is within the ERC prompt and toggle text conversion. +If `text-conversion-style' is not `action' if point is within the +prompt or `nil' otherwise, set it to such a value, so as to +guarantee that the input method functions properly for the +purpose of typing within the ERC prompt." + (when (and (eq major-mode 'erc-mode) + (fboundp 'set-text-conversion-style)) + (defvar text-conversion-style) ; avoid free variable warning on <=29 + (if (>= (point) (erc-beg-of-input-line)) + (unless (eq text-conversion-style 'action) + (set-text-conversion-style 'action)) + (unless (not text-conversion-style) + (set-text-conversion-style nil))))) + (defun erc-kill-server () "Sends a QUIT command to the server when the server buffer is killed. This function should be on `erc-kill-server-hook'." @@ -7302,11 +9554,12 @@ This function should be on `erc-kill-channel-hook'." (text-property-not-all (point-min) (point-max) 'erc-parsed nil)) (defun erc-restore-text-properties () - "Restore the property `erc-parsed' for the region." - (let ((parsed-posn (erc-find-parsed-property))) - (put-text-property - (point-min) (point-max) - 'erc-parsed (when parsed-posn (erc-get-parsed-vector parsed-posn))))) + "Ensure the `erc-parsed' and `tags' props cover the entire message." + (when-let ((parsed-posn (erc-find-parsed-property)) + (found (erc-get-parsed-vector parsed-posn))) + (put-text-property (point-min) (point-max) 'erc-parsed found) + (when-let ((tags (get-text-property parsed-posn 'tags))) + (put-text-property (point-min) (point-max) 'tags tags)))) (defun erc-get-parsed-vector (point) "Return the whole parsed vector on POINT." @@ -7326,6 +9579,13 @@ This function should be on `erc-kill-channel-hook'." (and vect (erc-response.command vect))) +(defun erc--get-eq-comparable-cmd (command) + "Return a symbol or a fixnum representing a message's COMMAND. +See also `erc-message-type'." + ;; IRC numerics are three-digit numbers, possibly with leading 0s. + ;; To invert: (if (numberp o) (format "%03d" o) (symbol-name o)) + (if-let ((n (string-to-number command)) ((zerop n))) (intern command) n)) + ;; Teach url.el how to open irc:// URLs with ERC. ;; To activate, customize `url-irc-function' to `url-irc-erc'. @@ -7364,6 +9624,8 @@ Beginning with ERC 5.5, new connections require human intervention. Customize `erc-url-connect-function' to override this." (when (eql port 0) (setq port nil)) (let* ((net (erc-networks--determine host)) + (erc--display-context `((erc-interactive-display . url) + ,@erc--display-context)) (server-buffer ;; Viable matches may slip through the cracks for unknown ;; networks. Additional passes could likely improve things. @@ -7409,6 +9671,4 @@ Customize `erc-url-connect-function' to override this." (provide 'erc) -;; FIXME this is a temporary stopgap for Emacs 29. -(require 'erc-goodies) ;;; erc.el ends here |