diff options
author | William Carroll <wpcarro@gmail.com> | 2018-09-10T18·51-0400 |
---|---|---|
committer | William Carroll <wpcarro@gmail.com> | 2018-09-10T18·53-0400 |
commit | 17ee0e400bef47c371afcae76037f9ea6a44ad13 (patch) | |
tree | 0e5efee6f00e402890e91f3eceb4b29408a498b6 /configs/shared/emacs/.emacs.d/elpa/circe-20180525.531/circe.el | |
parent | 8b2fadf4776b7ddb4a67b4bc8ff6463770e56028 (diff) |
Support Vim, Tmux, Emacs with Stow
After moving off of Meta, Dotfiles has a greater responsibility to manage configs. Vim, Tmux, and Emacs are now within Stow's purview.
Diffstat (limited to 'configs/shared/emacs/.emacs.d/elpa/circe-20180525.531/circe.el')
-rw-r--r-- | configs/shared/emacs/.emacs.d/elpa/circe-20180525.531/circe.el | 3602 |
1 files changed, 3602 insertions, 0 deletions
diff --git a/configs/shared/emacs/.emacs.d/elpa/circe-20180525.531/circe.el b/configs/shared/emacs/.emacs.d/elpa/circe-20180525.531/circe.el new file mode 100644 index 000000000000..721044dd2aff --- /dev/null +++ b/configs/shared/emacs/.emacs.d/elpa/circe-20180525.531/circe.el @@ -0,0 +1,3602 @@ +;;; circe.el --- Client for IRC in Emacs -*- lexical-binding: t -*- + +;; Copyright (C) 2005 - 2015 Jorgen Schaefer + +;; Version: 2.10 +;; Keywords: IRC, chat +;; Author: Jorgen Schaefer <forcer@forcix.cx> +;; URL: https://github.com/jorgenschaefer/circe + +;; This file is part of Circe. + +;; This program is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see <http://www.gnu.org/licenses/>. + +;;; Commentary: + +;; Circe is a Client for IRC in Emacs. It integrates well with the rest +;; of the editor, using standard Emacs key bindings and indicating +;; activity in channels in the status bar so it stays out of your way +;; unless you want to use it. + +;;; Code: + +(defvar circe-version "2.10" + "Circe version string.") + +(require 'circe-compat) + +(require 'ring) +(require 'timer) +(require 'lui) +(require 'lui-format) +(require 'lcs) +(require 'irc) + +;; Used to be optional. But sorry, we're in the 21st century already. +(require 'lui-irc-colors) + +;; necessary for inheriting from diff-added and diff-removed faces +(require 'diff-mode) + +(defgroup circe nil + "Yet Another Emacs IRC Client." + :prefix "circe-" + :group 'applications) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; Customization Options ;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;;;;;;;;;;;;;;; +;;;; Faces ;;;; +;;;;;;;;;;;;;;; + +(defface circe-prompt-face + '((t (:weight bold :foreground "Black" :background "LightSeaGreen"))) + "The face for the Circe prompt." + :group 'circe) + +(defface circe-server-face + '((((type tty)) (:foreground "blue" :weight bold)) + (((background dark)) (:foreground "#5095cf")) + (((background light)) (:foreground "#3840b0")) + (t (:foreground "SteelBlue"))) + "The face used to highlight server messages." + :group 'circe) + +(defface circe-highlight-nick-face + '((default (:weight bold)) + (((type tty)) (:foreground "cyan")) + (((background dark)) (:foreground "#82e2ed")) + (((background light)) (:foreground "#0445b7")) + (t (:foreground "CadetBlue3"))) + "The face used to highlight messages directed to us." + :group 'circe) + +(defface circe-my-message-face '((t)) + "The face used to highlight our own messages." + :group 'circe) + +(defface circe-originator-face '((t)) + "The face used to highlight the originator of a message." + :group 'circe) + +(defface circe-topic-diff-new-face '((t (:inherit diff-added))) + "The face used for text added to a topic. +See the {topic-diff} parameter to `circe-format-server-topic'." + :group 'circe) + +(defface circe-topic-diff-removed-face '((t (:inherit diff-removed))) + "The face used for text removed from a topic. +See the {topic-diff} parameter to `circe-format-server-topic'." + :group 'circe) + +(defface circe-fool-face + '((((type tty)) (:foreground "grey40" :bold t)) + (t (:foreground "grey40"))) + "The face used for fools. +See `circe-fool-list'." + :group 'circe) + +;;;;;;;;;;;;;;;;;;; +;;;; Variables ;;;; +;;;;;;;;;;;;;;;;;;; + +(defcustom circe-default-nick (user-login-name) + "The default nick for circe." + :type 'string + :group 'circe) + +(defcustom circe-default-user circe-default-nick + "The default user name for circe." + :type 'string + :group 'circe) + +(defcustom circe-default-realname (if (string= (user-full-name) "") + circe-default-nick + (user-full-name)) + "The default real name for circe." + :type 'string + :group 'circe) + +(defcustom circe-default-ip-family nil + "Default IP family to use. + + 'nil - Use either IPv4 or IPv6. + + 'ipv4 - Use IPv4 + + 'ipv6 - Use IPv6" + :type '(choice (const :tag "Both" nil) + (const :tag "IPv4" ipv4) + (const :tag "IPv6" ipv6)) + :group 'circe) + +(defcustom circe-default-directory "~/" + "The value of `default-directory' for Circe buffers." + :type 'string + :group 'circe) + +(defcustom circe-network-options nil + "Network options. + +This alist maps network names to respective options. + +Common options: + + :pass - The IRC server password to use for this network, or a + function to fetch it. + :nick - The nick name to use (defaults to `circe-default-nick') + :user - The user name to use (defaults to `circe-default-user') + :realname - The real name to use (defaults to `circe-default-realname') + + :channels - A plist of channels to join (see `circe-channels'). + :server-buffer-name - Format to be used for the server buffer name + (see `circe-server-buffer-name') + + :host - The host name of the server to connect to. + :port - The port or service name for the server. + :use-tls - A boolean indicating as to whether to use TLS or + not (defaults to nil). If you set this, you'll likely + have to set :port as well. + :ip-family - Option to enforce a specific IP version + (defaults to `circe-default-ip-family') + + :nickserv-nick - The nick to authenticate with to nickserv, if configured. + (defaults to the value of :nick) + :nickserv-password - The password to use for nickserv + authentication or a function to fetch it. + + :sasl-username - The username for SASL authentication. + :sasl-password - The password for SASL authentication." + :type '(alist :key-type string :value-type plist) + :group 'circe) + +(defvar circe-network-defaults + '(("Freenode" :host "irc.freenode.net" :port (6667 . 6697) + :tls t + :nickserv-mask "^NickServ!NickServ@services\\.$" + :nickserv-identify-challenge "\C-b/msg\\s-NickServ\\s-identify\\s-<password>\C-b" + :nickserv-identify-command "PRIVMSG NickServ :IDENTIFY {nick} {password}" + :nickserv-identify-confirmation "^You are now identified for .*\\.$" + :nickserv-ghost-command "PRIVMSG NickServ :GHOST {nick} {password}" + :nickserv-ghost-confirmation "has been ghosted\\.$\\|is not online\\.$" + ) + ("Coldfront" :host "irc.coldfront.net" :port 6667 + :nickserv-mask "^NickServ!services@coldfront\\.net$" + :nickserv-identify-challenge "/msg\\s-NickServ\\s-IDENTIFY\\s-\C-_password\C-_" + :nickserv-identify-command "PRIVMSG NickServ :IDENTIFY {password}" + ) + ("Bitlbee" :host "localhost" :port 6667 + :nickserv-mask "\\(bitlbee\\|root\\)!\\(bitlbee\\|root\\)@" + :nickserv-identify-challenge "use the \x02identify\x02 command to identify yourself" + :nickserv-identify-command "PRIVMSG &bitlbee :identify {password}" + :nickserv-identify-confirmation "Password accepted, settings and accounts loaded" + :lagmon-disabled t + ) + ("OFTC" :host "irc.oftc.net" :port (6667 . 6697) + :nickserv-mask "^NickServ!services@services\\.oftc\\.net$" + :nickserv-identify-challenge "This nickname is registered and protected." + :nickserv-identify-command "PRIVMSG NickServ :IDENTIFY {password} {nick}" + :nickserv-identify-confirmation "^You are successfully identified as .*\\.$" + ) + ) + "Alist of networks and connection settings. + +See the `circe' command for details of this variable.") + +(defcustom circe-default-quit-message "Using Circe, the loveliest of all IRC clients" + "The default quit message when no other is given. + +This is sent when the server buffer is killed or when /QUIT is +given with no argument." + :type 'string + :group 'circe) + +(defcustom circe-default-part-message "Using Circe, the loveliest of all IRC clients" + "How to part when a channel buffer is killed, or when no +argument is given to /PART." + :type 'string + :group 'circe) + +(defcustom circe-auto-query-max 23 + "The maximum number of queries which are opened automatically. +If more messages arrive - typically in a flood situation - they +are displayed in the server buffer." + :type 'integer + :group 'circe) + +(defcustom circe-use-cycle-completion nil + "Whether Circe should use cycle completion. + +If this is not nil, Circe will set `completion-cycle-threshold' +to t locally in Circe buffers, enabling cycle completion for +nicks no matter what completion style you use in the rest of +Emacs. If you set this to nil, Circe will not touch your default +completion style." + :type 'boolean + :group 'circe) + +(defcustom circe-reduce-lurker-spam nil + "If enabled, Circe will stop showing some messages. + +This means that JOIN, PART, QUIT and NICK messages are not shown +for users on channels that have not spoken yet (\"lurker\"), or +haven't spoken in `circe-active-users-timeout' seconds. When they +speak for the first time, Circe displays their join time." + :type 'boolean + :group 'circe) + +(defcustom circe-active-users-timeout nil + "When non-nil, should be the number of seconds after which +active users are regarded as inactive again after speaking." + :type 'integer + :group 'circe) + +(defcustom circe-prompt-string (concat (propertize ">" + 'face 'circe-prompt-face) + " ") + "The string to initialize the prompt with. +To change the prompt dynamically or just in specific buffers, use +`lui-set-prompt' in the appropriate hooks." + :type 'string + :group 'circe) + +(defcustom circe-extra-nicks nil + "List of other nicks than your current one to highlight." + :type '(repeat string) + :group 'circe) + +(defcustom circe-highlight-nick-type 'sender + "How to highlight occurrences of our own nick. + + 'sender - Highlight the nick of the sender + (messages without a sender and your + own are highlighted with the occurrence + type instead) + 'occurrence - Highlight the occurrences of the nick + 'message - Highlight the message without the sender + 'all - Highlight the whole line" + :type '(choice (const :tag "Sender" sender) + (const :tag "Occurrences" occurrence) + (const :tag "Message" message) + (const :tag "Whole line" all)) + :group 'circe) + +(defcustom circe-inhibit-nick-highlight-function nil + "Function for inhibiting nick highlighting. +If non-nil, its value is called with the respective buffer +selected and point in the line that's about to get highlighted. +A non-nil return value inhibits any highlighting." + :type '(choice (const :tag "None" nil) + function) + :group 'circe) + +(defcustom circe-completion-suffix ": " + "A suffix for completed nicks at the beginning of a line." + :type '(choice (const :tag "The standard suffix" ": ")) + :group 'circe) + +(defcustom circe-ignore-list nil + "List of regular expressions to ignore. + +Each regular expression is matched against nick!user@host." + :type '(repeat regexp) + :group 'circe) + +(defcustom circe-fool-list nil + "List of regular expressions for fools. + +Each regular expression is matched against nick!user@host. + +Messages from such people are still inserted, but not shown. They +can be displayed using \\[lui-fool-toggle-display]." + :type '(repeat regexp) + :group 'circe) + +(defcustom circe-ignore-functions nil + "A list of functions to check whether we should ignore a message. + +These functions get three arguments: NICK, USERHOST, and BODY. If +one of them returns a non-nil value, the message is ignored." + :type 'hook + :group 'circe) + +(defcustom circe-split-line-length 440 + "The maximum length of a single message. +If a message exceeds this size, it is broken into multiple ones. + +IRC allows for lines up to 512 bytes. Two of them are CR LF. +And a typical message looks like this: + + :nicky!uhuser@host212223.dialin.fnordisp.net PRIVMSG #lazybastards :Hello! + +You can limit here the maximum length of the \"Hello!\" part. +Good luck." + :type 'integer + :group 'circe) + +(defcustom circe-server-max-reconnect-attempts 5 + "How often Circe should attempt to reconnect to the server. +If this is 0, Circe will not reconnect at all. If this is nil, +it will try to reconnect forever (not recommended)." + :type '(choice integer + (const :tag "Forever" nil)) + :group 'circe) + +(defcustom circe-netsplit-delay 60 + "The number of seconds a netsplit may be dormant. +If anything happens with a netsplit after this amount of time, +the user is re-notified." + :type 'number + :group 'circe) + +(defcustom circe-server-killed-confirmation 'ask-and-kill-all + "How to ask for confirmation when a server buffer is killed. +This can be one of the following values: + ask - Ask the user for confirmation + ask-and-kill-all - Ask the user, and kill all associated buffers + kill-all - Don't ask the user, and kill all associated buffers + nil - Kill first, ask never" + :type '(choice (const :tag "Ask before killing" ask) + (const :tag "Ask, then kill all associated buffers" + ask-and-kill-all) + (const :tag "Don't ask, then kill all associated buffers" + kill-all) + (const :tag "Don't ask" nil)) + :group 'circe) + +(defcustom circe-channel-killed-confirmation 'ask + "How to ask for confirmation when a channel buffer is killed. +This can be one of the following values: + ask - Ask the user for confirmation + nil - Don't ask, just kill" + :type '(choice (const :tag "Ask before killing" ask) + (const :tag "Don't ask" nil)) + :group 'circe) + +(defcustom circe-track-faces-priorities '(circe-highlight-nick-face + lui-highlight-face + circe-my-message-face + circe-server-face) + "A list of faces which should show up in the tracking. +The first face is kept if the new message has only lower faces, +or faces that don't show up at all." + :type '(repeat face) + :group 'circe) + +(defcustom circe-server-send-unknown-command-p nil + "Non-nil when Circe should just pass on commands it doesn't know. +E.g. /fnord foo bar would then just send \"fnord foo bar\" to the +server." + :type 'boolean + :group 'circe) + +(defcustom circe-server-connected-hook nil + "Hook run when we successfully connected to a server. +This is run from a 001 (RPL_WELCOME) message handler." + :type 'hook + :group 'circe) + +(defcustom circe-server-auto-join-default-type :immediate + "The default auto-join type to use. + +Possible options: + +:immediate - Immediately after registering on the server +:after-auth - After nickserv authentication succeeded +:after-cloak - After we have acquired a cloaked host name +:after-nick - After we regained our preferred nick, or after + nickserv authentication if we don't need to regain + it. See `circe-nickserv-ghost-style'. + +See `circe-channels' for more details." + :type '(choice (const :tag "Immediately" :immediate) + (const :tag "After Authentication" :after-auth) + (const :tag "After Cloaking" :after-cloak) + (const :tag "After Nick Regain" :after-nick)) + :group 'circe) + +;;;;;;;;;;;;;;;;; +;;;; Formats ;;;; +;;;;;;;;;;;;;;;;; + +(defgroup circe-format nil + "Format strings for Circe. +All these formats always allow the {mynick} and {chattarget} format +strings." + :prefix "circe-format-" + :group 'circe) + +(defcustom circe-format-not-tracked + '(circe-format-server-message + circe-format-server-notice + circe--irc-format-server-numeric + circe-format-server-topic + circe-format-server-rejoin + circe-format-server-lurker-activity + circe-format-server-topic-time + circe-format-server-topic-time-for-channel + circe-format-server-netmerge + circe-format-server-join + circe-format-server-join-in-channel + circe-format-server-mode-change + circe-format-server-nick-change-self + circe-format-server-nick-change + circe-format-server-nick-regain + circe-format-server-part + circe-format-server-netsplit + circe-format-server-quit-channel + circe-format-server-quit) + "A list of formats that should not trigger tracking." + :type '(repeat symbol) + :group 'circe-format) + +(defcustom circe-format-server-message "*** {body}" + "The format for generic server messages. +{body} - The body of the message." + :type 'string + :group 'circe-format) + +(defcustom circe-format-self-say "> {body}" + "The format for messages to queries or channels. +{nick} - Your nick. +{body} - The body of the message." + :type 'string + :group 'circe-format) + +(defcustom circe-format-self-action "* {nick} {body}" + "The format for actions to queries or channels. +{nick} - Your nick. +{body} - The body of the action." + :type 'string + :group 'circe-format) + +(defcustom circe-format-self-message "-> *{chattarget}* {body}" + "The format for messages sent to other people outside of queries. +{chattarget} - The target nick. +{body} - The body of the message." + :type 'string + :group 'circe-format) + +(defcustom circe-format-action "* {nick} {body}" + "The format for actions in queries or channels. +{nick} - The nick doing the action. +{body} - The body of the action." + :type 'string + :group 'circe-format) + +(defcustom circe-format-message-action "* *{nick}* {body}" + "The format for actions in messages outside of queries. +{nick} - The nick doing the action. +{body} - The body of the action." + :type 'string + :group 'circe-format) + +(defcustom circe-format-say "<{nick}> {body}" + "The format for normal channel or query talk. +{nick} - The nick talking. +{body} - The message." + :type 'string + :group 'circe-format) + +(defcustom circe-format-message "*{nick}* {body}" + "The format for a message outside of a query. +{nick} - The originator. +{body} - The message." + :type 'string + :group 'circe-format) + +(defcustom circe-format-notice "-{nick}- {body}" + "The format for a notice. +{nick} - The originator. +{body} - The notice." + :type 'string + :group 'circe-format) + +(defcustom circe-format-server-notice "-Server Notice- {body}" + "The format for a server notice. +{body} - The notice." + :type 'string + :group 'circe-format) + +(defcustom circe-format-server-topic "*** Topic change by {nick} ({userhost}): {new-topic}" + "The format for topic changes. + +The following format arguments are available: + + nick - The nick of the user who changed the topic + userhost - The user@host string of that user + channel - Where the topic change happened + new-topic - The new topic + old-topic - The previous topic + topic-diff - A colorized diff of the topics" + :type 'string + :group 'circe-format) + +(defcustom circe-format-server-lurker-activity + "*** First activity: {nick} joined {joindelta} ago." + "The format for the first-activity notice of a user. +{nick} - The originator. +{jointime} - The join time of the user (in seconds). +{joindelta} - The duration from joining until now." + :type 'string + :group 'circe-format) + +(defcustom circe-format-server-rejoin + "*** Re-join: {nick} ({userinfo}), left {departuredelta} ago" + "The format for the re-join notice of a user. + +The following format arguments are available: + + nick - The nick of the user who joined + userhost - The user@host string of the user who joined + accountname - The account name, if the server supports this + realname - The real name, if the server supports this + userinfo - A combination of userhost, accountname, and realname + channel - A date string describing this time + departuretime - Time in seconds when the originator had left. + departuredelta - Description of the time delta since the originator left." + :type 'string + :group 'circe-format) + +(defcustom circe-server-buffer-name "{host}:{port}" + "The format for the server buffer name. + +The following format arguments are available: + + network - The name of the network + host - The host name of the server + port - The port number or service name + service - Alias for port" + :type 'string + :group 'circe-format) + +(defcustom circe-format-server-whois-idle-with-signon "*** {whois-nick} is {idle-duration} idle (signon on {signon-date}, {signon-ago} ago)" + "Format for RPL_WHOISIDLE messages. + +The following format arguments are available: + + whois-nick - The nick this is about + idle-seconds - The number of seconds this nick has been idle + idle-duration - A textual description of the duration of the idle time + signon-time - The time (in seconds since the epoch) when this user + signed on + signon-date - A date string describing this time + signon-ago - A textual description of the duraction since signon" + :type 'string + :group 'circe-format) + +(defcustom circe-format-server-whois-idle "*** {whois-nick} is {idle-duration} idle" + "Format for RPL_WHOISIDLE messages. + +The following format arguments are available: + + whois-nick - The nick this is about + idle-seconds - The number of seconds this nick has been idle + idle-duration - A textual description of the duration of the idle time" + :type 'string + :group 'circe-format) + +(defcustom circe-format-server-topic-time "*** Topic set by {setter} on {topic-date}, {topic-ago} ago" + "Format for RPL_TOPICWHOTIME messages for the current channel. + +The following format arguments are available: + + channel - The channel the topic is for + setter - The nick of the person who set the topic + setter-userhost - The user@host string of the person who set the topic + topic-time - The time the topic was set, in seconds since the epoch + topic-date - A date string describing this time + topic-ago - A textual description of the duration since the topic + was set" + :type 'string + :group 'circe-format) + +(defcustom circe-format-server-topic-time-for-channel "*** Topic for {channel} set by {setter} on {topic-date}, {topic-ago} ago" + "Format for RPL_TOPICWHOTIME messages for a channel we are not on. + +The following format arguments are available: + + channel - The channel the topic is for + setter - The nick of the person who set the topic + setter-userhost - The user@host string of the person who set the topic + topic-time - The time the topic was set, in seconds since the epoch + topic-date - A date string describing this time + topic-ago - A textual description of the duration since the topic + was set" + :type 'string + :group 'circe-format) + +(defcustom circe-format-server-channel-creation-time "*** Channel {channel} created on {date}, {ago} ago" + "Format for RPL_CREATIONTIME messages for the current channel. + +The following format arguments are available: + + channel - The channel the topic is for + date - A date string describing this time + ago - A textual description of the duration since the channel + was created" + :type 'string + :group 'circe-format) + +(defcustom circe-format-server-ctcp-ping "*** CTCP PING request from {nick} ({userhost}) to {target}: {body} ({ago} ago)" + "Format for CTCP PING requests. + +The following format arguments are available: + + nick - The nick of the user who sent this PING request + userhost - The user@host string of the user who sent this request + target - The target of the message, usually us, but can be a channel + body - The argument of the PING request, usually a number + ago - A textual description of the duration since the request + was sent, if parseable" + :type 'string + :group 'circe-format) + +(defcustom circe-format-server-ctcp-ping-reply "*** CTCP PING reply from {nick} ({userhost}) to {target}: {ago} ago ({body})" + "Format for CTCP PING replies. + +The following format arguments are available: + + nick - The nick of the user who sent this PING request + userhost - The user@host string of the user who sent this request + target - The target of the message, usually us, but can be a channel + body - The argument of the PING request, usually a number + ago - A textual description of the duration since the request + was sent, if parseable" + :type 'string + :group 'circe-format) + +(defcustom circe-format-server-ctcp "*** CTCP {command} request from {nick} ({userhost}) to {target}: {body}" + "Format for CTCP requests. + +The following format arguments are available: + + nick - The nick of the user who sent this PING request + userhost - The user@host string of the user who sent this request + target - The target of the message, usually us, but can be a channel + command - The CTCP command used + body - The argument of the PING request, usually a number" + :type 'string + :group 'circe-format) + +(defcustom circe-format-server-netsplit "*** Netsplit: {split} (Use /WL to see who left)" + "Format for netsplit notifications. + +The following format arguments are available: + + split - The name of the split, usually describing the servers involved" + :type 'string + :group 'circe-format) + +(defcustom circe-format-server-netmerge "*** Netmerge: {split}, split {ago} ago (Use /WL to see who's still missing)" + "Format for netmerge notifications. + +The following format arguments are available: + + split - The name of the split, usually describing the servers involved + time - The time when this split happened, in seconds + date - A date string describing this time + ago - A textual description of the duration since the split happened" + :type 'string + :group 'circe-format) + +(defcustom circe-format-server-join "*** Join: {nick} ({userinfo})" + "Format for join messages in a channel buffer. + +The following format arguments are available: + + nick - The nick of the user joining + userhost - The user@host string for the user + accountname - The account name, if the server supports this + realname - The real name, if the server supports this + userinfo - A combination of userhost, accountname, and realname + channel - The channel this user is joining" + :type 'string + :group 'circe-format) + +(defcustom circe-format-server-join-in-channel "*** Join: {nick} ({userinfo}) joined {channel}" + "Format for join messages in query buffers of the joining user. + +The following format arguments are available: + + nick - The nick of the user joining + userhost - The user@host string for the user + accountname - The account name, if the server supports this + realname - The real name, if the server supports this + userinfo - A combination of userhost, accountname, and realname + channel - The channel this user is joining" + :type 'string + :group 'circe-format) + +(defcustom circe-format-server-mode-change "*** Mode change: {change} on {target} by {setter} ({userhost})" + "Format for mode changes. + +The following format arguments are available: + + setter - The name of the split, usually describing the servers involved + userhost - The user@host string for the user + target - The target of this mode change + change - The actual changed modes" + :type 'string + :group 'circe-format) + +(defcustom circe-format-server-nick-change-self "*** Nick change: You are now known as {new-nick}" + "Format for nick changes of the current user. + +The following format arguments are available: + + old-nick - The old nick this change was from + new-nick - The new nick this change was to + userhost - The user@host string for the user" + :type 'string + :group 'circe-format) + +(defcustom circe-format-server-nick-change "*** Nick change: {old-nick} ({userhost}) is now known as {new-nick}" + "Format for nick changes of the current user. + +The following format arguments are available: + + old-nick - The old nick this change was from + new-nick - The new nick this change was to + userhost - The user@host string for the user" + :type 'string + :group 'circe-format) + +(defcustom circe-format-server-nick-regain "*** Nick regain: {old-nick} ({userhost}) is now known as {new-nick}" + "Format for nick changes of the current user. + +The following format arguments are available: + + old-nick - The old nick this change was from + new-nick - The new nick this change was to + userhost - The user@host string for the user" + :type 'string + :group 'circe-format) + +(defcustom circe-format-server-part "*** Part: {nick} ({userhost}) left {channel}: {reason}" + "Format for users parting a channel. + +The following format arguments are available: + + nick - The nick of the user who left + userhost - The user@host string for this user + channel - The channel they left + reason - The reason they gave for leaving" + :type 'string + :group 'circe-format) + +(defcustom circe-format-server-quit-channel "*** Quit: {nick} ({userhost}) left {channel}: {reason}" + "Format for users quitting from a channel. + +The following format arguments are available: + + nick - The nick of the user who left + userhost - The user@host string for this user + channel - The channel they left + reason - The reason they gave for leaving" + :type 'string + :group 'circe-format) + +(defcustom circe-format-server-quit "*** Quit: {nick} ({userhost}) left IRC: {reason}" + "Format for users quitting. + +The following format arguments are available: + + nick - The nick of the user who left + userhost - The user@host string for this user + reason - The reason they gave for leaving" + :type 'string + :group 'circe-format) + +;;;;;;;;;;;;;;;;;;;;;;;;; +;;; Private variables ;;; +;;;;;;;;;;;;;;;;;;;;;;;;; + +(defvar circe-source-url "https://github.com/jorgenschaefer/circe" + "URL to Circe's source repository") + +(defvar circe-host nil + "The name of the server we're currently connected to.") +(make-variable-buffer-local 'circe-host) + +(defvar circe-port nil + "The port number or service name of the server.") +(make-variable-buffer-local 'circe-host) + +(defvar circe-network nil + "The network name of the server we're currently connected to.") +(make-variable-buffer-local 'circe-network) + +(defvar circe-ip-family nil + "The IP family in use. +See `make-network-process' and :family for valid values.") +(make-variable-buffer-local 'circe-ip-family) + +(defvar circe-nick nil + "Our current nick.") +(make-variable-buffer-local 'circe-nick) + +(defvar circe-user nil + "The current user name.") +(make-variable-buffer-local 'circe-user) + +(defvar circe-realname nil + "The current real name.") +(make-variable-buffer-local 'circe-realname) + +(defvar circe-pass nil + "The password for the current server or a function to recall it. + +If a function is set it will be called with the value of `circe-host'.") +(make-variable-buffer-local 'circe-pass) + +(defvar circe-sasl-username nil + "The username for SASL authentication.") +(make-variable-buffer-local 'circe-sasl-username) + +(defvar circe-sasl-password nil + "The password for SASL authentication. + +If a function is set it will be called with the value of +`circe-host'.") +(make-variable-buffer-local 'circe-sasl-password) + +(defvar circe-use-tls nil + "If non-nil, use `open-tls-stream' to connect to the server.") +(make-variable-buffer-local 'circe-use-tls) + +(defvar circe-server-process nil + "The process of the server connection.") +(make-variable-buffer-local 'circe-server-process) + +(defvar circe-server-last-active-buffer nil + "The last active circe buffer.") +(make-variable-buffer-local 'circe-server-last-active-buffer) + +(defvar circe-display-table nil + "A hash table mapping commands to their display functions.") + +(defvar circe-server-inhibit-auto-reconnect-p nil + "Non-nil when Circe should not reconnect. + +This can be set from commands to avoid reconnecting when the +server disconnects.") +(make-variable-buffer-local 'circe-server-inhibit-auto-reconnect-p) + +(defvar circe-chat-calling-server-buffer-and-target nil + "Internal variable to pass the server buffer and target to chat modes.") + +(defvar circe-chat-target nil + "The current target for the buffer. +This is either a channel or a nick name.") +(make-variable-buffer-local 'circe-chat-target) + +(defvar circe-nick-syntax-table + (let ((table (make-syntax-table text-mode-syntax-table)) + (special (string-to-list "[]\`_^{}|-"))) + (dolist (char special) + (modify-syntax-entry char "w" table)) + table) + "Syntax table to treat nicks as words. +This is not entirely accurate, as exact chars constituting a nick +can vary between networks.") + +(defvar circe-nickserv-mask nil + "The regular expression to identify the nickserv on this network. + +Matched against nick!user@host.") +(make-variable-buffer-local 'circe-nickserv-mask) + +(defvar circe-nickserv-identify-challenge nil + "A regular expression matching the nickserv challenge to identify.") +(make-variable-buffer-local 'circe-nickserv-identify-challenge) + +(defvar circe-nickserv-identify-command nil + "The IRC command to send to identify with nickserv. + +This must be a full IRC command. It accepts the following +formatting options: + + {nick} - The nick to identify as + {password} - The configured nickserv password") +(make-variable-buffer-local 'circe-nickserv-identify-command) + +(defvar circe-nickserv-identify-confirmation nil + "A regular expression matching a confirmation of authentication.") +(make-variable-buffer-local 'circe-nickserv-identify-confirmation) + +(defvar circe-nickserv-ghost-command nil + "The IRC command to send to regain/ghost your nick. + +This must be a full IRC command. It accepts the following +formatting options: + + {nick} - The nick to ghost + {password} - The configured nickserv password") +(make-variable-buffer-local 'circe-nickserv-ghost-command) + +(defvar circe-nickserv-ghost-confirmation nil + "A regular expression matching a confirmation for the GHOST command. + +This is used to know when we can set our nick to the regained one +Leave nil if regaining automatically sets your nick") +(make-variable-buffer-local 'circe-nickserv-ghost-confirmation) + +(defvar circe-nickserv-nick nil + "The nick we are registered with for nickserv. + +Do not set this variable directly. Use `circe-network-options' or +pass an argument to the `circe' function for this.") +(make-variable-buffer-local 'circe-nickserv-nick) + +(defvar circe-nickserv-password nil + "The password we use for nickserv on this network. + +Can be either a string or a unary function of the nick returning +a string. + +Do not set this variable directly. Use `circe-network-options' or +pass an argument to the `circe' function for this.") +(make-variable-buffer-local 'circe-nickserv-password) + +(defvar circe-channels nil + "The default channels to join on this server. + +Don't set this variable by hand, use `circe-network-options'. + +The value should be a list of channels to join, with optional +keywords to configure the behavior of the following channels. + +Best explained in an example: + +\(\"#emacs\" :after-auth \"#channel\" \"#channel2\") + +Possible keyword options are: + +:immediate - Immediately after registering on the server +:after-auth - After nickserv authentication succeeded +:after-cloak - After we have acquired a cloaked host name +:after-nick - After we regained our preferred nick, or after + nickserv authentication if we don't need to regain + it. See `circe-nickserv-ghost-style'. + +The default is set in `circe-server-auto-join-default-type'. + +A keyword in the first position of the channels list overrides +`circe-server-auto-join-default-type' for re-joining manually +joined channels.") +(make-variable-buffer-local 'circe-channels) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;; Server Buffer Management ;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;; Every Circe buffer has an associated server buffer (which might be +;; the buffer itself). Circe buffers should set the +;; `circe-server-buffer' variable to the associated server buffer. + +(defun circe-server-buffer () + "Return the server buffer for the current buffer." + (let ((buf (if (eq major-mode 'circe-server-mode) + (current-buffer) + circe-server-buffer))) + (cond + ((not buf) + (error "Not in a Circe buffer")) + ((not (buffer-live-p buf)) + (error "The server buffer died, functionality is limited")) + (t + buf)))) + +(defmacro with-circe-server-buffer (&rest body) + "Run BODY with the current buffer being the current server buffer." + (declare (indent 0)) + `(with-current-buffer (circe-server-buffer) + ,@body)) + +;;;;;;;;;;;;;;;;;;;;;;; +;;; Editor Commands ;;; +;;;;;;;;;;;;;;;;;;;;;;; + +;;;###autoload +(defun circe-version () + "Display Circe's version." + (interactive) + (message "Circe %s" (circe--version))) + +(defun circe--version () + "Return Circe's version" + (let ((circe-git-version (circe--git-version))) + (if circe-git-version + (format "%s-%s" circe-version circe-git-version) + (format "%s" circe-version)))) + +(defun circe--git-version () + (let ((current-file-path (or load-file-name buffer-file-name))) + (when (or (not current-file-path) + (not (equal (file-name-nondirectory current-file-path) + "circe.el"))) + (setq current-file-path (locate-library "circe.el"))) + (let ((vcs-path (locate-dominating-file current-file-path ".git"))) + (when vcs-path + (let ((default-directory vcs-path)) + ;; chop off the trailing newline + (substring (shell-command-to-string "git rev-parse --short HEAD") + 0 -1)))))) + +;;;###autoload +(defun circe (network-or-server &rest server-options) + "Connect to IRC. + +Connect to the given network specified by NETWORK-OR-SERVER. + +When this function is called, it collects options from the +SERVER-OPTIONS argument, the user variable +`circe-network-options', and the defaults found in +`circe-network-defaults', in this order. + +If NETWORK-OR-SERVER is not found in any of these variables, the +argument is assumed to be the host name for the server, and all +relevant settings must be passed via SERVER-OPTIONS. + +All SERVER-OPTIONS are treated as variables by getting the string +\"circe-\" prepended to their name. This variable is then set +locally in the server buffer. + +See `circe-network-options' for a list of common options." + (interactive (circe--read-network-and-options)) + (let* ((options (circe--server-get-network-options network-or-server + server-options)) + (buffer (circe--server-generate-buffer options))) + (with-current-buffer buffer + (circe-server-mode) + (circe--server-set-variables options) + (circe-reconnect)) + (pop-to-buffer-same-window buffer))) + +(defun circe--read-network-and-options () + "Read a host or network name with completion. + +If it's not a network, also read some extra options. + +This uses `circe-network-defaults' and `circe-network-options' for +network names." + (let ((default-network (if (null circe-network-options) + (caar circe-network-defaults) + (caar circe-network-options))) + (networks nil) + (completion-ignore-case t) + network-or-host) + (dolist (network-spec (append circe-network-options + circe-network-defaults)) + (when (not (member (car network-spec) networks)) + (push (car network-spec) networks))) + (setq networks (sort networks 'string-lessp)) + (setq network-or-host (completing-read "Network or host: " + networks + nil nil nil nil + default-network)) + (dolist (network-name networks) + (when (equal (downcase network-or-host) + (downcase network-name)) + (setq network-or-host network-name))) + (if (member network-or-host networks) + (list network-or-host) + (list network-or-host + :host network-or-host + :port (read-number "Port: " 6667))))) + +(defun circe--server-get-network-options (network server-options) + "Combine server and network options with network defaults. + +See `circe-network-options' and `circe-network-defaults'." + (let ((options (mapcar 'circe--translate-option-names + (append server-options + (cdr (assoc network circe-network-options)) + (cdr (assoc network circe-network-defaults)) + (list :network network))))) + (when (not (plist-get options :host)) + (plist-put options :host network)) + (let ((port (plist-get options :port)) + (use-tls (plist-get options :use-tls))) + (when (consp port) + (if use-tls + (plist-put options :port (cdr port)) + (plist-put options :port (car port))))) + (dolist (required-option '(:host :port)) + (when (not (plist-get options required-option)) + (error (format "Network option %s not specified" required-option)))) + options)) + +(defun circe--translate-option-names (option) + "Translate option names to make them unique. + +Some options have multiple names, mainly for historical reasons. +Unify them here." + (cond + ((eq option :service) :port) + ((eq option :tls) :use-tls) + ((eq option :family) :ip-family) + (t option))) + +(defun circe--server-generate-buffer (options) + "Return the server buffer for the connection described in OPTIONS." + (let* ((network (plist-get options :network)) + (host (plist-get options :host)) + (port (plist-get options :port)) + (buffer-name (lui-format (or (plist-get options :server-buffer-name) + circe-server-buffer-name) + :network network + :host host + :port port + :service port))) + (generate-new-buffer buffer-name))) + +(defun circe--server-set-variables (options) + "Set buffer-local variables described in OPTIONS. + +OPTIONS is a plist as passed to `circe'. All options therein are +set as buffer-local variables. Only the first occurrence of each +variable is set." + (setq circe-nick circe-default-nick + circe-user circe-default-user + circe-realname circe-default-realname + circe-ip-family circe-default-ip-family) + (let ((done nil) + (todo options)) + (while todo + (when (not (memq (car todo) done)) + (push (car todo) done) + (let ((var (intern (format "circe-%s" + (substring (symbol-name (car todo)) 1)))) + (val (cadr todo))) + (if (boundp var) + (set (make-local-variable var) val) + (warn "Unknown option %s, ignored" (car todo))))) + (setq todo (cddr todo))))) + +(defvar circe-server-reconnect-attempts 0 + "The number of reconnect attempts that Circe has done so far. +See `circe-server-max-reconnect-attempts'.") +(make-variable-buffer-local 'circe-server-reconnect-attempts) + +(defun circe-reconnect () + "Reconnect the current server." + (interactive) + (with-circe-server-buffer + (when (or (called-interactively-p 'any) + (circe--reconnect-p)) + (setq circe-server-inhibit-auto-reconnect-p t + circe-server-reconnect-attempts (+ circe-server-reconnect-attempts + 1)) + (unwind-protect + (circe-reconnect--internal) + (setq circe-server-inhibit-auto-reconnect-p nil))))) + +(defun circe--reconnect-p () + (cond + (circe-server-inhibit-auto-reconnect-p + nil) + ((not circe-server-max-reconnect-attempts) + t) + ((<= circe-server-reconnect-attempts + circe-server-max-reconnect-attempts) + t) + (t + nil))) + +(defun circe-reconnect--internal () + "The internal function called for reconnecting unconditionally. + +Do not use this directly, use `circe-reconnect'" + (when (and circe-server-process + (process-live-p circe-server-process)) + (delete-process circe-server-process)) + (circe-display-server-message "Connecting...") + (dolist (buf (circe-server-chat-buffers)) + (with-current-buffer buf + (circe-display-server-message "Connecting..."))) + (setq circe-server-process + (irc-connect + :host circe-host + :service circe-port + :tls circe-use-tls + :ip-family circe-ip-family + :handler-table (circe-irc-handler-table) + :server-buffer (current-buffer) + :nick circe-nick + :nick-alternatives (list (circe--nick-next circe-nick) + (circe--nick-next + (circe--nick-next circe-nick))) + :user circe-user + :mode 8 + :realname circe-realname + :pass (if (functionp circe-pass) + (funcall circe-pass circe-host) + circe-pass) + :cap-req (append (when (and circe-sasl-username + circe-sasl-password) + '("sasl")) + '("extended-join")) + :nickserv-nick (or circe-nickserv-nick + circe-nick) + :nickserv-password (if (functionp circe-nickserv-password) + (funcall circe-nickserv-password circe-host) + circe-nickserv-password) + :nickserv-mask circe-nickserv-mask + :nickserv-identify-challenge circe-nickserv-identify-challenge + :nickserv-identify-command circe-nickserv-identify-command + :nickserv-identify-confirmation + circe-nickserv-identify-confirmation + :nickserv-ghost-command circe-nickserv-ghost-command + :nickserv-ghost-confirmation circe-nickserv-ghost-confirmation + :sasl-username circe-sasl-username + :sasl-password (if (functionp circe-sasl-password) + (funcall circe-sasl-password + circe-host) + circe-sasl-password) + :ctcp-version (format "Circe: Client for IRC in Emacs, version %s" + circe-version) + :ctcp-source circe-source-url + :ctcp-clientinfo "CLIENTINFO PING SOURCE TIME VERSION" + :auto-join-after-registration + (append (circe--auto-join-channel-buffers) + (circe--auto-join-list :immediate)) + :auto-join-after-host-hiding + (circe--auto-join-list :after-cloak) + :auto-join-after-nick-acquisition + (circe--auto-join-list :after-nick) + :auto-join-after-nickserv-identification + (circe--auto-join-list :after-auth) + :auto-join-after-sasl-login + (circe--auto-join-list :after-auth)))) + +(defun circe-reconnect-all () + "Reconnect all Circe connections." + (interactive) + (dolist (buf (circe-server-buffers)) + (with-current-buffer buf + (if (called-interactively-p 'any) + (call-interactively 'circe-reconnect) + (circe-reconnect))))) + +(defun circe--auto-join-list (type) + "Return the list of channels to join for type TYPE." + (let ((result nil) + (current-type circe-server-auto-join-default-type)) + (dolist (channel circe-channels) + (cond + ((keywordp channel) + (setq current-type channel)) + ((eq current-type type) + (push channel result)))) + (nreverse result))) + +(defun circe--auto-join-channel-buffers () + "Return a list of channels to join based on channel buffers. + +This includes all channel buffers of the current server, but +excludes and channel that is already listed in +`circe-channels'." + (let ((channels nil)) + (dolist (buf (circe-server-chat-buffers)) + (let ((name (with-current-buffer buf + (when (derived-mode-p 'circe-channel-mode) + circe-chat-target)))) + (when (and name + (not (member name circe-channels))) + (push name channels)))) + channels)) + +;;;;;;;;;;;;;;;;; +;;; Base Mode ;;; +;;;;;;;;;;;;;;;;; + +(defvar circe-mode-hook nil + "Hook run for any Circe mode.") + +(defvar circe-mode-map + (let ((map (make-sparse-keymap))) + (define-key map (kbd "C-c C-j") 'circe-command-JOIN) + (define-key map (kbd "C-c C-r") 'circe-reconnect) + map) + "The base keymap for all Circe modes (server, channel, query)") + +(defvar circe-server-buffer nil + "The buffer of the server associated with the current chat buffer.") +(make-variable-buffer-local 'circe-server-buffer) + +(define-derived-mode circe-mode lui-mode "Circe" + "Base mode for all Circe buffers. + +A buffer should never be in this mode directly, but rather in +modes that derive from this. + +The mode inheritance hierarchy looks like this: + +lui-mode +`-circe-mode + `-circe-server-mode + `-circe-chat-mode + `-circe-channel-mode + `-circe-query-mode" + (add-hook 'lui-pre-output-hook 'lui-irc-colors + t t) + (add-hook 'lui-pre-output-hook 'circe--output-highlight-nick + t t) + (add-hook 'completion-at-point-functions 'circe--completion-at-point + nil t) + (lui-set-prompt circe-prompt-string) + (goto-char (point-max)) + (setq lui-input-function 'circe--input + default-directory (expand-file-name circe-default-directory) + circe-server-last-active-buffer (current-buffer) + flyspell-generic-check-word-p 'circe--flyspell-check-word-p) + (when circe-use-cycle-completion + (set (make-local-variable 'completion-cycle-threshold) + t)) + ;; Tab completion should be case-insensitive + (set (make-local-variable 'completion-ignore-case) + t) + (set (make-local-variable 'tracking-faces-priorities) + circe-track-faces-priorities)) + +;;;;;;;;;;;;;;;;;;;; +;;;; Displaying ;;;; +;;;;;;;;;;;;;;;;;;;; + +(defun circe-display (format &rest keywords) + "Display FORMAT formatted with KEYWORDS in the current Circe buffer. +See `lui-format' for a description of the format. + +If FORMAT contains the word server, the resulting string receives +a `circe-server-face'. If FORMAT contains the word self, the +whole string receives a `circe-my-message-face'. If FORMAT is in +`circe-format-not-tracked', a message of this type is never +tracked by Lui. + +Keywords with the name :nick receive a `circe-originator-face'. + +It is always possible to use the mynick or target formats." + (when (not (circe--display-ignored-p format keywords)) + (let* ((name (symbol-name format)) + (face (cond + ((string-match "\\<server\\>" name) + 'circe-server-face) + ((string-match "\\<self\\>" name) + 'circe-my-message-face))) + (keywords (append `(:mynick ,(circe-nick) + :chattarget ,circe-chat-target) + (circe--display-add-nick-property + (if (and (not (null keywords)) + (null (cdr keywords))) + (car keywords) + keywords)))) + (text (lui-format format keywords))) + (when (circe--display-fool-p format keywords) + (add-face-text-property 0 (length text) + 'circe-fool-face t text) + (put-text-property 0 (length text) + 'lui-fool t + text)) + (when face + (add-face-text-property 0 (length text) + face t text)) + (lui-insert text + (memq format circe-format-not-tracked))))) + +(defun circe-display-server-message (message) + "Display MESSAGE as a server message." + (circe-display 'circe-format-server-message + :body message)) + +(defun circe--display-add-nick-property (keywords) + "Add a face to the value of the :nick property in KEYWORDS." + (let ((keyword nil)) + (mapcar (lambda (entry) + (cond + ((or (eq keyword :nick) + (eq keyword 'nick)) + (setq keyword nil) + (propertize entry 'face 'circe-originator-face)) + (t + (setq keyword entry) + entry))) + keywords))) + +(defun circe--display-ignored-p (_format keywords) + (let ((nick (plist-get keywords :nick)) + (userhost (plist-get keywords :userhost)) + (body (plist-get keywords :body))) + (circe--ignored-p nick userhost body))) + +(defun circe--display-fool-p (_format keywords) + (let ((nick (plist-get keywords :nick)) + (userhost (plist-get keywords :userhost)) + (body (plist-get keywords :body))) + (circe--fool-p nick userhost body))) + +(defun circe--ignored-p (nick userhost body) + "True if this user or message is being ignored. + +See `circe-ignore-functions' and `circe-ignore-list'. + +NICK, USER and HOST should be the sender of a the command +COMMAND, which had the arguments ARGS." + (or (run-hook-with-args-until-success 'circe-ignore-functions + nick userhost body) + (circe--ignore-matches-p nick userhost body circe-ignore-list))) + +(defun circe--fool-p (nick userhost body) + "True if this user or message is a fool. + +See `circe-fool-list'. + +NICK, USER and HOST should be the sender of a the command +COMMAND, which had the arguments ARGS." + (circe--ignore-matches-p nick userhost body circe-fool-list)) + +(defun circe--ignore-matches-p (nick userhost body patterns) + "Check if a given command does match an ignore pattern. + +A pattern matches if it either matches the user NICK!USER@HOST, +or if it matches the first word in BODY. + +PATTERNS should be the list of regular expressions." + (let ((string (format "%s!%s" nick userhost)) + (target (when (and body + (string-match "^\\([^ ]*\\)[:,]" body)) + (match-string 1 body)))) + (catch 'return + (dolist (regex patterns) + (when (string-match regex string) + (throw 'return t)) + (when (and (stringp target) + (string-match regex target)) + (throw 'return t))) + nil))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;; Nick Highlighting ;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defun circe--output-highlight-nick () + "Highlight the nick of the user in the buffer. + +This is used in `lui-pre-output-hook'." + (goto-char (or (text-property-any (point-min) (point-max) + 'lui-format-argument 'body) + (point-min))) + (when (or (not circe-inhibit-nick-highlight-function) + (not (funcall circe-inhibit-nick-highlight-function))) + (let* ((nick (circe-nick)) + (nicks (append (and nick (list nick)) + circe-extra-nicks))) + (when nicks + ;; Can't use \<...\> because that won't match for \<forcer-\> We + ;; might eventually use \_< ... \_> if we define symbols to be + ;; nicks \\= is necessary, because it might be found right where we + ;; are, and that might not be the beginning of a line... (We start + ;; searching from the beginning of the body) + (let ((nick-regex (concat "\\(?:^\\|\\W\\|\\=\\)" + "\\(" (regexp-opt nicks) "\\)" + "\\(?:$\\|\\W\\)"))) + (cond + ((eq circe-highlight-nick-type 'sender) + (if (text-property-any (point-min) + (point-max) + 'face 'circe-originator-face) + (when (re-search-forward nick-regex nil t) + (circe--extend-text-having-face + (point-min) (point-max) + 'circe-originator-face + 'circe-highlight-nick-face)) + (let ((circe-highlight-nick-type 'occurrence)) + (circe--output-highlight-nick)))) + ((eq circe-highlight-nick-type 'occurrence) + (while (re-search-forward nick-regex nil t) + (add-face-text-property (match-beginning 1) + (match-end 1) + 'circe-highlight-nick-face))) + ((eq circe-highlight-nick-type 'message) + (when (re-search-forward nick-regex nil t) + (let* ((start (text-property-any (point-min) + (point-max) + 'lui-format-argument 'body)) + (end (when start + (next-single-property-change start + 'lui-format-argument)))) + (when (and start end) + (add-face-text-property start end + 'circe-highlight-nick-face))))) + ((eq circe-highlight-nick-type 'all) + (when (re-search-forward nick-regex nil t) + (add-face-text-property (point-min) (point-max) + 'circe-highlight-nick-face))))))))) + +(defun circe--extend-text-having-face (from to existing new) + "Extend property values. + +In the text between FROM and TO, find any text that has its face +property set to EXISTING, and prepend NEW to the value of its +face property, when necessary by turning it into a list." + (let ((beg (text-property-any from to 'face existing))) + (while beg + (let ((end (next-single-property-change beg 'face))) + (add-face-text-property beg end new) + (setq beg (text-property-any end to 'face existing)))))) + +;;;;;;;;;;;;;;; +;;;; Input ;;;; +;;;;;;;;;;;;;;; + +(defun circe--input (str) + "Process STR as input. + +This detects commands and interprets them, or sends the input +using the /SAY command." + (set-text-properties 0 (length str) nil str) + (cond + ((string= str "") + nil) + ;; Ignore commands in multiline input + ((and (not (string-match "\n" str)) + (string-match "\\`/\\([^/ ][^ ]*\\|[^/ ]*\\) ?\\([^\n]*\\)\\'" str)) + (let* ((command (match-string 1 str)) + (args (match-string 2 str)) + (handler (intern-soft (format "circe-command-%s" + (upcase command))))) + (cond + ((string= command "") + (circe-command-SAY args)) + (handler + (funcall handler args)) + (circe-server-send-unknown-command-p + (irc-send-raw (circe-server-process) + (format "%s %s" + (upcase command) + args))) + (t + (circe-display-server-message (format "Unknown command: %s" + command)))))) + (t + (mapc #'circe-command-SAY + (circe--list-drop-right (split-string str "\n") + "^ *$"))))) + +;;;;;;;;;;;;;;;;;; +;;;; Flyspell ;;;; +;;;;;;;;;;;;;;;;;; + +(defun circe--flyspell-check-word-p () + "Return a true value if flyspell check the word before point. + +This is a suitable value for `flyspell-generic-check-word-p'. It +will also call `lui-flyspell-check-word-p'." + (cond + ((not (lui-flyspell-check-word-p)) + nil) + ((circe-channel-user-p (circe--flyspell-nick-before-point)) + nil) + (t + t))) + +(defun circe--flyspell-nick-before-point () + "Return the IRC nick before point" + (with-syntax-table circe-nick-syntax-table + (let (beg end) + (save-excursion + (forward-word -1) + (setq beg (point)) + (forward-word 1) + (setq end (point))) + (buffer-substring beg end)))) + +;;;;;;;;;;;;;;;;;;;; +;;;; Completion ;;;; +;;;;;;;;;;;;;;;;;;;; + +(defun circe--completion-at-point () + "Return a list of possible completions for the current buffer. + +This is used in `completion-at-point-functions'." + ;; Use markers so they move when input happens + (let ((start (make-marker)) + (end (make-marker))) + (set-marker end (point)) + (set-marker start + (save-excursion + (when (or (looking-back (regexp-quote + circe-completion-suffix) + (length circe-completion-suffix)) + (looking-back " " 1)) + (goto-char (match-beginning 0))) + (cond + ((<= (point) lui-input-marker) + lui-input-marker) + ((re-search-backward "\\s-" lui-input-marker t) + (1+ (point))) + (t + lui-input-marker)))) + (list start end 'circe--completion-table))) + +(defun circe--completion-table (string pred action) + "Completion table to use for Circe buffers. + +See `minibuffer-completion-table' for details." + (cond + ;; Best completion of STRING + ((eq action nil) + (try-completion string + (circe--completion-candidates + (if (= (- (point) (length string)) + lui-input-marker) + circe-completion-suffix + " ")) + pred)) + ;; A list of possible completions of STRING + ((eq action t) + (all-completions string + (circe--completion-candidates + (if (= (- (point) (length string)) + lui-input-marker) + circe-completion-suffix + " ")) + pred)) + ;; t iff STRING is a valid completion as it stands + ((eq action 'lambda) + (test-completion string + (circe--completion-candidates + (if (= (- (point) (length string)) + lui-input-marker) + circe-completion-suffix + " ")) + pred)) + ;; Boundaries + ((eq (car-safe action) 'boundaries) + `(boundaries 0 . ,(length (cdr action)))) + ;; Metadata + ((eq action 'metadata) + '(metadata (cycle-sort-function . circe--completion-sort))))) + +(defun circe--completion-clean-nick (string) + (with-temp-buffer + (insert string) + (goto-char (point-max)) + (when (or (looking-back circe-completion-suffix nil) + (looking-back " " nil)) + (replace-match "")) + (buffer-string))) + +(defun circe--completion-sort (collection) + "Sort the COLLECTION by channel activity for nicks." + (let* ((proc (circe-server-process)) + (channel (when (and circe-chat-target proc) + (irc-connection-channel proc circe-chat-target))) + (decorated (mapcar (lambda (entry) + (let* ((nick (circe--completion-clean-nick + entry)) + (user (when channel + (irc-channel-user channel nick)))) + (list (when user + (irc-user-last-activity-time user)) + (length entry) + entry))) + collection)) + (sorted (sort decorated + (lambda (a b) + (cond + ((and (car a) + (car b)) + (> (car a) + (car b))) + ((and (not (car a)) + (not (car b))) + (< (cadr a) + (cadr b))) + ((car a) + t) + (t + nil)))))) + (mapcar (lambda (entry) + (nth 2 entry)) + sorted))) + +;; FIXME: I do not know why this is here. +(defvar circe--completion-old-completion-all-sorted-completions nil + "Variable to know if we can return a cached result.") +(make-variable-buffer-local + 'circe--completion-old-completion-all-sorted-completions) +(defvar circe--completion-cache nil + "The results we can cache.") +(make-variable-buffer-local 'circe--completion-cache) + +(defun circe--completion-candidates (nick-suffix) + (if (and circe--completion-old-completion-all-sorted-completions + (eq completion-all-sorted-completions + circe--completion-old-completion-all-sorted-completions)) + circe--completion-cache + (let ((completions (append (circe--commands-list) + (mapcar (lambda (buf) + (with-current-buffer buf + circe-chat-target)) + (circe-server-channel-buffers))))) + (cond + ;; In a server buffer, complete all nicks in all channels + ((eq major-mode 'circe-server-mode) + (dolist (buf (circe-server-channel-buffers)) + (with-current-buffer buf + (dolist (nick (circe-channel-nicks)) + (setq completions (cons (concat nick nick-suffix) + completions)))))) + ;; In a channel buffer, only complete nicks in this channel + ((eq major-mode 'circe-channel-mode) + (setq completions (append (delete (concat (circe-nick) + nick-suffix) + (mapcar (lambda (nick) + (concat nick nick-suffix)) + (circe-channel-nicks))) + completions))) + ;; In a query buffer, only complete this query partner + ((eq major-mode 'circe-query-mode) + (setq completions (cons (concat circe-chat-target nick-suffix) + completions))) + ;; Else, we're doing something wrong + (t + (error "`circe-possible-completions' called outside of Circe"))) + (setq circe--completion-old-completion-all-sorted-completions + completion-all-sorted-completions + circe--completion-cache completions) + completions))) + +(defun circe--commands-list () + "Return a list of possible Circe commands." + (mapcar (lambda (symbol) + (let ((str (symbol-name symbol))) + (if (string-match "^circe-command-\\(.*\\)" str) + (concat "/" (match-string 1 str) " ") + str))) + (apropos-internal "^circe-command-"))) + +;;;;;;;;;;;;;;;;;;; +;;; Server Mode ;;; +;;;;;;;;;;;;;;;;;;; + +(defvar circe-server-mode-hook nil + "Hook run when a new Circe server buffer is created.") + +(defvar circe-server-mode-map (make-sparse-keymap) + "The key map for server mode buffers.") + +(define-derived-mode circe-server-mode circe-mode "Circe Server" + "The mode for circe server buffers. + +This buffer represents a server connection. When you kill it, the +server connection is closed. This will make all associated +buffers unusable. Be sure to use \\[circe-reconnect] if you want +to reconnect to the server. + +\\{circe-server-mode-map}" + (add-hook 'kill-buffer-hook 'circe-server-killed nil t)) + +(defun circe-server-killed () + "Run when the server buffer got killed. + +This will IRC, and ask the user whether to kill all of the +server's chat buffers." + (when circe-server-killed-confirmation + (when (not (y-or-n-p + (if (eq circe-server-killed-confirmation 'ask-and-kill-all) + "Really kill all buffers of this server? (if not, try `circe-reconnect') " + "Really kill the IRC connection? (if not, try `circe-reconnect') "))) + (error "Buffer not killed as per user request"))) + (setq circe-server-inhibit-auto-reconnect-p t) + (ignore-errors + (irc-send-QUIT circe-server-process circe-default-quit-message)) + (ignore-errors + (delete-process circe-server-process)) + (when (or (eq circe-server-killed-confirmation 'ask-and-kill-all) + (eq circe-server-killed-confirmation 'kill-all)) + (dolist (buf (circe-server-chat-buffers)) + (let ((circe-channel-killed-confirmation nil)) + (kill-buffer buf))))) + +(defun circe-server-buffers () + "Return a list of all server buffers in this Emacs instance." + (let ((result nil)) + (dolist (buf (buffer-list)) + (with-current-buffer buf + (when (eq major-mode 'circe-server-mode) + (setq result (cons buf result))))) + (nreverse result))) + +(defun circe-server-process () + "Return the current server process." + (with-circe-server-buffer + circe-server-process)) + +(defun circe-server-my-nick-p (nick) + "Return non-nil when NICK is our current nick." + (let ((proc (circe-server-process))) + (when proc + (irc-current-nick-p proc nick)))) + +(defun circe-nick () + "Return our current nick." + (let ((proc (circe-server-process))) + (when proc + (irc-current-nick proc)))) + +(defun circe-server-last-active-buffer () + "Return the last active buffer of this server." + (with-circe-server-buffer + (if (and circe-server-last-active-buffer + (bufferp circe-server-last-active-buffer) + (buffer-live-p circe-server-last-active-buffer)) + circe-server-last-active-buffer + (current-buffer)))) + +;; There really ought to be a hook for this +(defadvice select-window (after circe-server-track-select-window + (window &optional norecord)) + "Remember the current buffer as the last active buffer. +This is used by Circe to know where to put spurious messages." + (with-current-buffer (window-buffer window) + (when (derived-mode-p 'circe-mode) + (let ((buf (current-buffer))) + (ignore-errors + (with-circe-server-buffer + (setq circe-server-last-active-buffer buf))))))) +(ad-activate 'select-window) + +(defun circe-reduce-lurker-spam () + "Return the value of `circe-reduce-lurker-spam'. + +This uses a buffer-local value first, then the one in the server +buffer. + +Use this instead of accessing the variable directly to enable +setting the variable through network options." + (if (local-variable-p 'circe-reduce-lurker-spam) + circe-reduce-lurker-spam + (with-circe-server-buffer + circe-reduce-lurker-spam))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;; Chat Buffer Management ;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;; Server buffers keep track of associated chat buffers. This enables +;; us to not rely on buffer names staying the same, as well as keeping +;; buffers from different servers and even server connections apart +;; cleanly. + +(defvar circe-server-chat-buffer-table nil + "A hash table of chat buffers associated with this server.") +(make-variable-buffer-local 'circe-server-chat-buffer-table) + +(defun circe-server-get-chat-buffer (target) + "Return the chat buffer addressing TARGET, or nil if none." + (with-circe-server-buffer + (when circe-server-chat-buffer-table + (let* ((target-name (irc-isupport--case-fold (circe-server-process) + target)) + (buf (gethash target-name circe-server-chat-buffer-table))) + (if (buffer-live-p buf) + buf + (remhash target-name circe-server-chat-buffer-table) + nil))))) + +(defun circe-server-create-chat-buffer (target chat-mode) + "Return a new buffer addressing TARGET in CHAT-MODE." + (with-circe-server-buffer + (let* ((target-name (irc-isupport--case-fold (circe-server-process) + target)) + (chat-buffer (generate-new-buffer target)) + (server-buffer (current-buffer)) + (circe-chat-calling-server-buffer-and-target (cons server-buffer + target-name))) + (when (not circe-server-chat-buffer-table) + (setq circe-server-chat-buffer-table (make-hash-table :test 'equal))) + (puthash target-name chat-buffer circe-server-chat-buffer-table) + (with-current-buffer chat-buffer + (funcall chat-mode)) + chat-buffer))) + +(defun circe-server-get-or-create-chat-buffer (target chat-mode) + "Return a buffer addressing TARGET; create one in CHAT-MODE if none exists." + (let ((buf (circe-server-get-chat-buffer target))) + (if buf + buf + (circe-server-create-chat-buffer target chat-mode)))) + +(defun circe-server-remove-chat-buffer (target-or-buffer) + "Remove the buffer addressing TARGET-OR-BUFFER." + (with-circe-server-buffer + (let* ((target (if (bufferp target-or-buffer) + (circe-server-chat-buffer-target target-or-buffer) + target-or-buffer)) + (target-name (irc-isupport--case-fold (circe-server-process) + target))) + (remhash target-name circe-server-chat-buffer-table)))) + +(defun circe-server-rename-chat-buffer (old-name new-name) + "Note that the chat buffer addressing OLD-NAME now addresses NEW-NAME." + (with-circe-server-buffer + (let* ((old-target-name (irc-isupport--case-fold (circe-server-process) + old-name)) + (new-target-name (irc-isupport--case-fold (circe-server-process) + new-name)) + (buf (gethash old-target-name circe-server-chat-buffer-table))) + (when buf + (remhash old-target-name circe-server-chat-buffer-table) + (puthash new-target-name buf circe-server-chat-buffer-table) + (with-current-buffer buf + (setq circe-chat-target new-name) + (rename-buffer new-name t)))))) + +(defun circe-server-chat-buffer-target (&optional buffer) + "Return the chat target of BUFFER, or the current buffer if none." + (if buffer + (with-current-buffer buffer + circe-chat-target) + circe-chat-target)) + +(defun circe-server-chat-buffers () + "Return the list of chat buffers on this server." + (with-circe-server-buffer + (when circe-server-chat-buffer-table + (let ((buffer-list nil)) + (maphash (lambda (target-name buffer) + (if (buffer-live-p buffer) + (push buffer buffer-list) + (remhash target-name circe-server-chat-buffer-table))) + circe-server-chat-buffer-table) + buffer-list)))) + +(defun circe-server-channel-buffers () + "Return a list of all channel buffers of this server." + (let ((result nil)) + (dolist (buf (circe-server-chat-buffers)) + (with-current-buffer buf + (when (eq major-mode 'circe-channel-mode) + (setq result (cons buf result))))) + (nreverse result))) + +;;;;;;;;;;;;;;;;; +;;; Chat Mode ;;; +;;;;;;;;;;;;;;;;; + +(defvar circe-chat-mode-hook nil + "Hook run when a new chat buffer (channel or query) is created.") + +(defvar circe-chat-mode-map (make-sparse-keymap) + "Base key map for all Circe chat buffers (channel, query).") + +;; Defined here as we use it, but do not necessarily want to use the +;; full module. +(defvar lui-logging-format-arguments nil + "A list of arguments to be passed to `lui-format'. +This can be used to extend the formatting possibilities of the +file name for lui applications.") +(make-variable-buffer-local 'lui-logging-format-arguments) + +(define-derived-mode circe-chat-mode circe-mode "Circe Chat" + "The circe chat major mode. + +This is the common mode used for both queries and channels. +It should not be used directly. +TARGET is the default target to send data to. +SERVER-BUFFER is the server buffer of this chat buffer." + (setq circe-server-buffer (car circe-chat-calling-server-buffer-and-target) + circe-chat-target (cdr circe-chat-calling-server-buffer-and-target)) + (let ((network (with-circe-server-buffer + circe-network))) + (make-local-variable 'mode-line-buffer-identification) + (setq mode-line-buffer-identification + (list (format "%%b@%-8s" network))) + (setq lui-logging-format-arguments + `(:target ,circe-chat-target :network ,network))) + (when (equal circe-chat-target "#emacs-circe") + (set (make-local-variable 'lui-button-issue-tracker) + "https://github.com/jorgenschaefer/circe/issues/%s"))) + +(defun circe-chat-disconnected () + "The current buffer got disconnected." + (circe-display-server-message "Disconnected")) + +;;;;;;;;;;;;;;;;;;;; +;;; Channel Mode ;;; +;;;;;;;;;;;;;;;;;;;; + +(defvar circe-channel-mode-hook nil + "Hook run in a new channel buffer.") + +(defvar circe-channel-mode-map + (let ((map (make-sparse-keymap))) + (define-key map (kbd "C-c C-n") 'circe-command-NAMES) + (define-key map (kbd "C-c C-t") 'circe-command-CHTOPIC) + map) + "The key map for channel mode buffers.") + +(define-derived-mode circe-channel-mode circe-chat-mode "Circe Channel" + "The circe channel chat major mode. +This mode represents a channel you are talking in. + +TARGET is the default target to send data to. +SERVER-BUFFER is the server buffer of this chat buffer. + +\\{circe-channel-mode-map}" + (add-hook 'kill-buffer-hook 'circe-channel-killed nil t)) + +(defun circe-channel-killed () + "Called when the channel buffer got killed. + +If we are not on the channel being killed, do nothing. Otherwise, +if the server is live, and the user wants to kill the buffer, +send PART to the server and clean up the channel's remaining +state." + (when (buffer-live-p circe-server-buffer) + (when (and circe-channel-killed-confirmation + (not (y-or-n-p "Really leave this channel? "))) + (error "Channel not left.")) + (ignore-errors + (irc-send-PART (circe-server-process) + circe-chat-target + circe-default-part-message)) + (ignore-errors + (circe-server-remove-chat-buffer circe-chat-target)))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;; Channel User Tracking ;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;; Channel mode buffers provide some utility functions to check if a +;; given user is idle or not. + +(defun circe-channel-user-nick-regain-p (_old new) + "Return true if a nick change from OLD to NEW constitutes a nick regain. + +A nick was regained if the NEW nick was also a recent user." + (let ((channel (irc-connection-channel (circe-server-process) + circe-chat-target))) + (when channel + (irc-channel-recent-user channel new)))) + +(defun circe-channel-user-p (nick) + "Return non-nil when NICK belongs to a channel user." + (cond + ((eq major-mode 'circe-query-mode) + (irc-string-equal-p (circe-server-process) + nick + circe-chat-target)) + ((eq major-mode 'circe-channel-mode) + (let ((channel (irc-connection-channel (circe-server-process) + circe-chat-target))) + (when channel + (if (irc-channel-user channel nick) + t + nil)))))) + +(defun circe-channel-nicks () + "Return a list of nicks in the current channel." + (let* ((channel (irc-connection-channel (circe-server-process) + circe-chat-target)) + (nicks nil)) + (when channel + (maphash (lambda (_folded-nick user) + (push (irc-user-nick user) nicks)) + (irc-channel-users channel))) + nicks)) + +(defun circe-user-channels (nick) + "Return a list of channel buffers for the user named NICK." + (let* ((result nil)) + (dolist (channel (irc-connection-channel-list (circe-server-process))) + (when (irc-channel-user channel nick) + (let* ((name (irc-channel-name channel)) + (buf (circe-server-get-chat-buffer name))) + (when buf + (push buf result))))) + result)) + +(defun circe-lurker-p (nick) + "Return a true value if this nick is regarded inactive." + (let* ((channel (irc-connection-channel (circe-server-process) + circe-chat-target)) + (user (when channel + (irc-channel-user channel nick))) + (recent-user (when channel + (irc-channel-recent-user channel nick))) + (last-active (cond + (user + (irc-user-last-activity-time user)) + (recent-user + (irc-user-last-activity-time recent-user))))) + (cond + ;; If we do not track lurkers, no one is ever a lurker. + ((not (circe-reduce-lurker-spam)) + nil) + ;; We ourselves are never lurkers (in this sense). + ((circe-server-my-nick-p nick) + nil) + ;; Someone who isn't even on the channel (e.g. NickServ) isn't a + ;; lurker, either. + ((and (not user) + (not recent-user)) + nil) + ;; If someone has never been active, they most definitely *are* a + ;; lurker. + ((not last-active) + t) + ;; But if someone has been active, and we mark active users + ;; inactive again after a timeout ... + (circe-active-users-timeout + ;; They are still lurkers if their activity has been too long + ;; ago. + (> (- (float-time) + last-active) + circe-active-users-timeout)) + ;; Otherwise, they have been active and we don't mark active + ;; users inactive again, so nope, not a lurker. + (t + nil)))) + +(defun circe-lurker-rejoin-p (nick channel) + "Return true if NICK is rejoining CHANNEL. + +A user is considered to be rejoining if they were on the channel +shortly before, and were active then." + (let* ((channel (irc-connection-channel (circe-server-process) + channel)) + (user (when channel + (irc-channel-recent-user channel nick)))) + (when user + (irc-user-last-activity-time user)))) + +(defun circe-lurker-display-active (nick userhost) + "Show that this user is active if they are a lurker." + (let* ((channel (irc-connection-channel (circe-server-process) + circe-chat-target)) + (user (when channel + (irc-channel-user channel nick))) + (join-time (when user + (irc-user-join-time user)))) + (when (and (circe-lurker-p nick) + ;; If we saw them when we joined the channel, no need to + ;; say "they're suddenly active!!111one". + join-time) + (circe-display 'circe-format-server-lurker-activity + :nick nick + :userhost (or userhost "server") + :jointime join-time + :joindelta (circe-duration-string + (- (float-time) + join-time)))))) + +;;;;;;;;;;;;;;;;;; +;;; Query Mode ;;; +;;;;;;;;;;;;;;;;;; + +(defvar circe-query-mode-hook nil + "Hook run when query mode is activated.") + +(defvar circe-query-mode-map + (let ((map (make-sparse-keymap))) + (set-keymap-parent map circe-chat-mode-map) + map) + "The key map for query mode buffers.") + +(define-derived-mode circe-query-mode circe-chat-mode "Circe Query" + "The circe query chat major mode. +This mode represents a query you are talking in. + +TARGET is the default target to send data to. +SERVER-BUFFER is the server buffer of this chat buffer. + +\\{circe-query-mode-map}" + (add-hook 'kill-buffer-hook 'circe-query-killed nil t)) + +(defun circe-query-killed () + "Called when the query buffer got killed." + (ignore-errors + (circe-server-remove-chat-buffer circe-chat-target))) + +(defun circe-query-auto-query-buffer (who) + "Return a buffer for a query with `WHO'. + +This adheres to `circe-auto-query-max'." + (or (circe-server-get-chat-buffer who) + (when (< (circe--query-count) + circe-auto-query-max) + (circe-server-get-or-create-chat-buffer who 'circe-query-mode)))) + +(defun circe--query-count () + "Return the number of queries on the current server." + (let ((num 0)) + (dolist (buf (circe-server-chat-buffers)) + (with-current-buffer buf + (when (eq major-mode 'circe-query-mode) + (setq num (+ num 1))))) + num)) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; IRC Protocol Handling ;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defvar circe--irc-handler-table nil + "The handler table for Circe's IRC connections. + +Do not use this directly. Instead, call `circe-irc-handler-table'.") + +(defun circe-irc-handler-table () + (when (not circe--irc-handler-table) + (let ((table (irc-handler-table))) + (irc-handler-add table "irc.registered" #'circe--irc-conn-registered) + (irc-handler-add table "conn.disconnected" #'circe--irc-conn-disconnected) + (irc-handler-add table nil #'circe--irc-display-event) + (irc-handle-registration table) + (irc-handle-ping-pong table) + (irc-handle-isupport table) + (irc-handle-initial-nick-acquisition table) + (irc-handle-ctcp table) + (irc-handle-state-tracking table) + (irc-handle-nickserv table) + (irc-handle-auto-join table) + (setq circe--irc-handler-table table))) + circe--irc-handler-table) + +(defun circe--irc-conn-registered (conn _event _nick) + (with-current-buffer (irc-connection-get conn :server-buffer) + (setq circe-server-reconnect-attempts 0) + (run-hooks 'circe-server-connected-hook))) + +(defun circe--irc-conn-disconnected (conn _event) + (with-current-buffer (irc-connection-get conn :server-buffer) + (dolist (buf (circe-server-chat-buffers)) + (with-current-buffer buf + (circe-chat-disconnected))) + + (circe-reconnect))) + +(defun circe--irc-display-event (conn event &optional sender &rest args) + "Display an IRC message. + +NICK, USER and HOST specify the originator of COMMAND with ARGS +as arguments." + (with-current-buffer (irc-connection-get conn :server-buffer) + (let* ((display (circe-get-display-handler event)) + (nick (when sender + (irc-userstring-nick sender))) + (userhost (when sender + (irc-userstring-userhost sender)))) + (cond + ;; Functions get called + ((functionp display) + (apply display nick userhost event args)) + ;; Lists describe patterns + ((consp display) + (circe--irc-display-format (elt display 1) + (elt display 0) + nick userhost event args)) + ;; No configured display handler, show a default + (t + (circe--irc-display-default nick userhost event args)))))) + +(defvar circe--irc-format-server-numeric "*** %s" + "The format to use for server messages. Do not set this.") + +(defun circe--irc-display-format (format target nick userhost event args) + (let* ((target+name (circe--irc-display-target target nick args)) + (target (car target+name)) + (name (cdr target+name)) + (origin (if userhost + (format "%s (%s)" nick userhost) + (format "%s" nick)))) + (with-current-buffer (or target + (circe-server-last-active-buffer)) + (let ((circe--irc-format-server-numeric + (if target + (format "*** %s" format) + (format "*** [%s] %s" name format)))) + (circe-display 'circe--irc-format-server-numeric + :nick (or nick "(unknown)") + :userhost (or userhost "server") + :origin origin + :event event + :command event + :target name + :indexed-args args))))) + +(defun circe--irc-display-target (target nick args) + "Return the target buffer and name. +The buffer might be nil if it is not alive. + +See `circe-set-display-handler' for a description of target. + +NICK and USERHOST are the originator of COMMAND which had ARGS as +arguments." + (cond + ((eq target 'nick) + (cons (circe-server-get-chat-buffer nick) + nick)) + ((numberp target) + (let ((name (nth target + args))) + (cons (circe-server-get-chat-buffer name) + name))) + ((eq target 'active) + (let ((buf (circe-server-last-active-buffer))) + (cons buf + (buffer-name buf)))) + ((eq target 'server) + (cons (current-buffer) (buffer-name))) + (t + (error "Bad target in format string: %s" target)))) + +(defun circe--irc-display-default (nick userhost event args) + (with-current-buffer (circe-server-last-active-buffer) + (let ((target (if (circe-server-my-nick-p (car args)) + "" + (format " to %s" (car args))))) + (cond + ((string-match "\\`irc.ctcpreply.\\(.*\\)\\'" event) + (circe-display-server-message + (format "CTCP %s reply from %s (%s)%s: %s" + (match-string 1 event) nick userhost target (cadr args)))) + ((string-match "\\`irc.ctcp.\\(.*\\)\\'" event) + (circe-display-server-message + (format "Unknown CTCP request %s from %s (%s)%s: %s" + (match-string 1 event) nick userhost target (cadr args)))) + (t + (circe-display-server-message + (format "[%s from %s%s] %s" + event + nick + (if userhost + (format " (%s)" userhost) + "") + (mapconcat #'identity args " ")))))))) + +(defun circe-set-display-handler (command handler) + "Set the display handler for COMMAND to HANDLER. + +A handler is either a function or a list. + +A function gets called in the server buffer with at least three +arguments, but possibly more. There's at least NICK and USERHOST +of the sender, which can be nil, and COMMAND, which is the event +which triggered this. Further arguments are arguments to the +event. + +Alternatively, the handler can be a list of two elements: + + target - The target of this message + format - The format for this string + +The target can be any of: + + 'active - The last active buffer of this server + 'nick - The nick who sent this message + 'server - The server buffer for this server + number - The index of the argument of the target + +The format is passed to `lui-format'. Possible format string +substitutions are {mynick}, {target}, {nick}, {userhost}, +{origin}, {command}, {target}, and indexed arguments for the +arguments to the IRC message." + (when (not circe-display-table) + (setq circe-display-table (make-hash-table :test 'equal))) + (puthash command handler circe-display-table)) + +(defun circe-get-display-handler (command) + "Return the display handler for COMMAND. + +See `circe-set-display-handler' for more information." + (when circe-display-table + (gethash command circe-display-table))) + +;;;;;;;;;;;;;;;; +;;; Commands ;;; +;;;;;;;;;;;;;;;; + +(defun circe-command-AWAY (reason) + "Set yourself away with REASON." + (interactive "sReason: ") + (irc-send-AWAY (circe-server-process) reason)) + +(defun circe-command-BACK (&optional ignored) + "Mark yourself not away anymore. + +Arguments are IGNORED." + (interactive) + (irc-send-AWAY (circe-server-process))) + +(defun circe-command-CHTOPIC (&optional ignored) + "Insert the topic of the current channel. + +Arguments are IGNORED." + (interactive) + (if (not circe-chat-target) + (circe-display-server-message "No target for current buffer") + (let* ((channel (irc-connection-channel (circe-server-process) + circe-chat-target)) + (topic (when channel + (irc-channel-topic channel)))) + (lui-replace-input (format "/TOPIC %s %s" + circe-chat-target (or topic "")))) + (goto-char (point-max)))) + +(defun circe-command-CLEAR (&optional ignored) + "Delete all buffer contents before the lui prompt." + (let ((inhibit-read-only t)) + (delete-region (point-min) lui-output-marker))) + +(defun circe-command-CTCP (who &optional command argument) + "Send a CTCP message to WHO containing COMMAND with ARGUMENT. +If COMMAND is not given, WHO is parsed to contain all of who, +command and argument. +If ARGUMENT is nil, it is interpreted as no argument." + (when (not command) + (if (string-match "^\\([^ ]*\\) *\\([^ ]*\\) *\\(.*\\)" who) + (setq command (upcase (match-string 2 who)) + argument (match-string 3 who) + who (match-string 1 who)) + (circe-display-server-message "Usage: /CTCP <who> <what>"))) + (when (not (string= "" command)) + (irc-send-ctcp (circe-server-process) + who + command + (if (and argument (not (equal argument ""))) + argument + nil)))) + +(defun circe-command-FOOL (line) + "Add the regex on LINE to the `circe-fool-list'." + (with-current-buffer (circe-server-last-active-buffer) + (cond + ((string-match "\\S-+" line) + (let ((regex (match-string 0 line))) + (add-to-list 'circe-fool-list regex) + (circe-display-server-message (format "Recognizing %s as a fool" + regex)))) + ((not circe-fool-list) + (circe-display-server-message "Your do not know any fools")) + (t + (circe-display-server-message "Your list of fools:") + (dolist (regex circe-fool-list) + (circe-display-server-message (format "- %s" regex))))))) + +(defun circe-command-GAWAY (reason) + "Set yourself away on all servers with reason REASON." + (interactive "sReason: ") + (dolist (buf (circe-server-buffers)) + (with-current-buffer buf + (irc-send-AWAY circe-server-process reason)))) + +(defun circe-command-GQUIT (reason) + "Quit all servers with reason REASON." + (interactive "sReason: ") + (dolist (buf (circe-server-buffers)) + (with-current-buffer buf + (when (eq (process-status circe-server-process) + 'open) + (irc-send-QUIT circe-server-process reason))))) + +(defun circe-command-HELP (&optional ignored) + "Display a list of recognized commands, nicely formatted." + (circe-display-server-message + (concat "Recognized commands are: " + (mapconcat (lambda (s) s) (circe--commands-list) "")))) + +(defun circe-command-IGNORE (line) + "Add the regex on LINE to the `circe-ignore-list'." + (with-current-buffer (circe-server-last-active-buffer) + (cond + ((string-match "\\S-+" line) + (let ((regex (match-string 0 line))) + (add-to-list 'circe-ignore-list regex) + (circe-display-server-message (format "Ignore list, meet %s" + regex)))) + ((not circe-ignore-list) + (circe-display-server-message "Your ignore list is empty")) + (t + (circe-display-server-message "Your ignore list:") + (dolist (regex circe-ignore-list) + (circe-display-server-message (format "- %s" regex))))))) + +(defun circe-command-INVITE (nick &optional channel) + "Invite NICK to CHANNEL. +When CHANNEL is not given, NICK is assumed to be a string +consisting of two words, the nick and the channel." + (interactive "sInvite who: \nsWhere: ") + (when (not channel) + (if (string-match "^\\([^ ]+\\) +\\([^ ]+\\)" nick) + (setq channel (match-string 2 nick) + nick (match-string 1 nick)) + (when (or (string= "" nick) (null nick)) + (circe-display-server-message "Usage: /INVITE <nick> <channel>")))) + (irc-send-INVITE (circe-server-process) + nick + (if (and (null channel) + (not (null nick))) + circe-chat-target + channel))) + +(defun circe-command-JOIN (channel) + "Join CHANNEL. This can also contain a key." + (interactive "sChannel: ") + (let (channels keys) + (when (string-match "^\\s-*\\([^ ]+\\)\\(:? \\([^ ]+\\)\\)?$" channel) + (setq channels (match-string 1 channel) + keys (match-string 3 channel)) + (dolist (channel (split-string channels ",")) + (pop-to-buffer + (circe-server-get-or-create-chat-buffer channel + 'circe-channel-mode))) + (irc-send-JOIN (circe-server-process) channels keys)))) + +(defun circe-command-ME (line) + "Send LINE to IRC as an action." + (interactive "sAction: ") + (if (not circe-chat-target) + (circe-display-server-message "No target for current buffer") + (circe-display 'circe-format-self-action + :body line + :nick (circe-nick)) + (irc-send-ctcp (circe-server-process) + circe-chat-target + "ACTION" line))) + +(defun circe-command-MSG (who &optional what) + "Send a message. + +Send WHO a message containing WHAT. + +If WHAT is not given, WHO should contain both the nick and the +message separated by a space." + (when (not what) + (if (string-match "^\\([^ ]*\\) \\(.*\\)" who) + (setq what (match-string 2 who) + who (match-string 1 who)) + (circe-display-server-message "Usage: /MSG <who> <what>"))) + (when what + (let ((buf (circe-query-auto-query-buffer who))) + (if buf + (with-current-buffer buf + (circe-command-SAY what) + (lui-add-input what)) + (with-current-buffer (circe-server-last-active-buffer) + (irc-send-PRIVMSG (circe-server-process) + who what) + (circe-display 'circe-format-self-message + :target who + :body what)))))) + +(defun circe-command-NAMES (&optional channel) + "List the names of the current channel or CHANNEL." + (interactive) + (let ((target (when channel + (string-trim channel)))) + (when (or (not target) + (equal target "")) + (setq target circe-chat-target)) + (if (not target) + (circe-display-server-message "No target for current buffer") + (irc-send-NAMES (circe-server-process) + target)))) + +(defun circe-command-NICK (newnick) + "Change nick to NEWNICK." + (interactive "sNew nick: ") + (let ((newnick (string-trim newnick))) + (irc-send-NICK (circe-server-process) newnick))) + +(defun circe-command-PART (reason) + "Part the current channel because of REASON." + (interactive "sReason: ") + (if (not circe-chat-target) + (circe-display-server-message "No target for current buffer") + (irc-send-PART (circe-server-process) + circe-chat-target + (if (equal "" reason) + circe-default-part-message + reason)))) + +(defun circe-command-PING (target) + "Send a CTCP PING request to TARGET." + (interactive "sWho: ") + (let ((target (string-trim target))) + (irc-send-ctcp (circe-server-process) + target + "PING" (format "%s" (float-time))))) + +(defun circe-command-QUERY (arg) + "Open a query with WHO." + ;; Eventually, this should probably be just the same as + ;; circe-command-MSG + (interactive "sQuery with: ") + (let* (who what) + (if (string-match "\\`\\s-*\\(\\S-+\\)\\s-\\(\\s-*\\S-.*\\)\\'" arg) + (setq who (match-string 1 arg) + what (match-string 2 arg)) + (setq who (string-trim arg))) + (when (string= who "") + (circe-display-server-message "Usage: /query <nick> [something to say]")) + (pop-to-buffer + (circe-server-get-or-create-chat-buffer who 'circe-query-mode)) + (when what + (circe-command-SAY what) + (lui-add-input what)))) + +(defun circe-command-QUIT (reason) + "Quit the current server giving REASON." + (interactive "sReason: ") + (with-circe-server-buffer + (setq circe-server-inhibit-auto-reconnect-p t) + (irc-send-QUIT (circe-server-process) + (if (equal "" reason) + circe-default-quit-message + reason)))) + +(defun circe-command-QUOTE (line) + "Send LINE verbatim to the server." + (interactive "Line: ") + (irc-send-raw (circe-server-process) line) + (with-current-buffer (circe-server-last-active-buffer) + (circe-display-server-message (format "Sent to server: %s" + line)))) + +(defun circe-command-SAY (line) + "Say LINE to the current target." + (interactive "sSay: ") + (if (not circe-chat-target) + (circe-display-server-message "No target for current buffer") + (dolist (line (circe--split-line line)) + (circe-display 'circe-format-self-say + :body line + :nick (circe-nick)) + (irc-send-PRIVMSG (circe-server-process) + circe-chat-target + ;; Some IRC servers give an error if there is + ;; no text at all. + (if (string= line "") + " " + line))))) + +(defun circe--split-line (longline) + "Splits LONGLINE into smaller components. + +IRC silently truncates long lines. This splits a long line into +parts that each are not longer than `circe-split-line-length'." + (if (< (length longline) + circe-split-line-length) + (list longline) + (with-temp-buffer + (insert longline) + (let ((fill-column circe-split-line-length)) + (fill-region (point-min) (point-max) + nil t)) + (split-string (buffer-string) "\n")))) + +(defun circe-command-SV (&optional ignored) + "Tell the current channel about your client and Emacs version. + +Arguments are IGNORED." + (interactive) + (circe-command-SAY (format (concat "I'm using Circe version %s " + "with %s %s (of %s)") + (circe--version) + "GNU Emacs" + emacs-version + (format-time-string "%Y-%m-%d" + emacs-build-time)))) + +(defun circe-command-TOPIC (channel &optional newtopic) + "Change the topic of CHANNEL to NEWTOPIC." + (interactive "sChannel: \nsNew topic: ") + (when (string-match "^\\s-*$" channel) + (setq channel nil)) + (when (and channel + (not newtopic) + (string-match "^\\s-*\\(\\S-+\\)\\( \\(.*\\)\\)?" channel)) + (setq newtopic (match-string 3 channel) + channel (match-string 1 channel))) + (cond + ((and channel newtopic) + (irc-send-TOPIC (circe-server-process) channel newtopic)) + (channel + (irc-send-TOPIC (circe-server-process) channel)) + (circe-chat-target + (irc-send-TOPIC (circe-server-process) circe-chat-target)) + (t + (circe-display-server-message "No channel given, and no default target.")))) + +(defun circe-command-UNFOOL (line) + "Remove the entry LINE from `circe-fool-list'." + (with-current-buffer (circe-server-last-active-buffer) + (cond + ((string-match "\\S-+" line) + (let ((regex (match-string 0 line))) + (setq circe-fool-list (delete regex circe-fool-list)) + (circe-display-server-message (format "Assuming %s is not a fool anymore" + regex)))) + (t + (circe-display-server-message + "No one is not a fool anymore? UNFOOL requires one argument"))))) + +(defun circe-command-UNIGNORE (line) + "Remove the entry LINE from `circe-ignore-list'." + (with-current-buffer (circe-server-last-active-buffer) + (cond + ((string-match "\\S-+" line) + (let ((regex (match-string 0 line))) + (setq circe-ignore-list (delete regex circe-ignore-list)) + (circe-display-server-message (format "Ignore list forgot about %s" + regex)))) + (t + (circe-display-server-message + "Who do you want to unignore? UNIGNORE requires one argument"))))) + +(defun circe-command-WHOAMI (&optional ignored) + "Request WHOIS information about yourself. + +Arguments are IGNORED." + (interactive) + (irc-send-WHOIS (circe-server-process) + (circe-nick))) + +(defun circe-command-WHOIS (whom) + "Request WHOIS information about WHOM." + (interactive "sWhois: ") + (let* ((whom-server-name (split-string whom)) + (whom (car whom-server-name)) + (server-or-name (cadr whom-server-name))) + (irc-send-WHOIS (circe-server-process) whom server-or-name))) + +(defun circe-command-WHOWAS (whom) + "Request WHOWAS information about WHOM." + (interactive "sWhois: ") + (let ((whom (string-trim whom))) + (irc-send-WHOWAS (circe-server-process) whom))) + +(defun circe-command-STATS (query) + "Request statistics from a server." + (interactive) + ;; Split string into query and server if we can + (let ((query (split-string query))) + (irc-send-STATS (circe-server-process) (car query) (cadr query)))) + +(defun circe-command-WL (&optional split) + "Show the people who left in a netsplit. +Without any arguments, shows shows the current netsplits and how +many people are missing. With an argument SPLIT, which must be a +number, it shows the missing people due to that split." + (let ((circe-netsplit-list (with-circe-server-buffer + circe-netsplit-list))) + (if (or (not split) + (and (stringp split) + (string= split ""))) + (if (null circe-netsplit-list) + (circe-display-server-message "No net split at the moment") + (let ((n 0)) + (dolist (entry circe-netsplit-list) + (circe-display-server-message (format "(%d) Missing %d people due to %s" + n + (hash-table-count (nth 3 entry)) + (car entry))) + (setq n (+ n 1))))) + (let* ((index (if (numberp split) + split + (string-to-number split))) + (entry (nth index circe-netsplit-list))) + (if (not entry) + (circe-display-server-message (format "No split number %s - use /WL to see a list" + split)) + (let ((missing nil)) + (maphash (lambda (_key value) + (setq missing (cons value missing))) + (nth 3 entry)) + (circe-display-server-message + (format "Missing people due to %s: %s" + (car entry) + (mapconcat 'identity + (sort missing + (lambda (a b) + (string< (downcase a) + (downcase b)))) + ", "))))))))) + +;;;;;;;;;;;;;;;;;;;;;;;; +;;; Display Handlers ;;; +;;;;;;;;;;;;;;;;;;;;;;;; + +(defun circe-display-ignore (_nick _userhost _command &rest _args) + "Don't show a this message. + +NICK and USERHOST are the originator of COMMAND which had ARGS as +arguments." + 'noop) + +(circe-set-display-handler "317" 'circe-display-317) +(defun circe-display-317 (_sender ignored _numeric _target nick + idletime &optional signon-time body) + "Show a 317 numeric (RPL_WHOISIDLE). + +Arguments are either of the two: + +:<server> 317 <ournick> <nick> <idle> :seconds idle +:<server> 317 <ournick> <nick> <idle> <signon> :seconds idle, signon time" + (with-current-buffer (circe-server-last-active-buffer) + (let ((seconds-idle (string-to-number idletime)) + (signon-time (when body + (string-to-number signon-time)))) + (if signon-time + (circe-display 'circe-format-server-whois-idle-with-signon + :whois-nick nick + :idle-seconds seconds-idle + :idle-duration (circe-duration-string seconds-idle) + :signon-time signon-time + :signon-date (current-time-string + (seconds-to-time signon-time)) + :signon-ago (circe-duration-string (- (float-time) + signon-time))) + (circe-display 'circe-format-server-whois-idle + :whois-nick nick + :idle-seconds seconds-idle + :idle-duration (circe-duration-string seconds-idle)))))) + +(circe-set-display-handler "329" 'circe-display-329) +(defun circe-display-329 (_server ignored _numeric _target channel timestamp) + "Show a 329 numeric (RPL_CREATIONTIME)." + (with-current-buffer (or (circe-server-get-chat-buffer channel) + (circe-server-last-active-buffer)) + (let ((creation-time (string-to-number timestamp))) + (circe-display 'circe-format-server-channel-creation-time + :channel channel + :date (current-time-string + (seconds-to-time creation-time)) + :ago (circe-duration-string (- (float-time) + creation-time)))))) + +(circe-set-display-handler "333" 'circe-display-333) +(defun circe-display-333 (_server ignored _numeric target + channel setter topic-time) + "Show a 333 numeric (RPL_TOPICWHOTIME). + +Arguments are either of the two: + +:<server> 333 <target> <channel> <nick> 1434996762 +:<server> 333 <target> <channel> <nick>!<user>@<host> 1434996803" + (let ((channel-buffer (circe-server-get-chat-buffer channel)) + (topic-time (string-to-number topic-time))) + (with-current-buffer (or channel-buffer + (circe-server-last-active-buffer)) + (circe-display (if channel-buffer + 'circe-format-server-topic-time + 'circe-format-server-topic-time-for-channel) + :nick target + :channel channel + :setter (irc-userstring-nick setter) + :setter-userhost (or (irc-userstring-userhost setter) + "(unknown)") + :topic-time topic-time + :topic-date (current-time-string + (seconds-to-time topic-time)) + :topic-ago (circe-duration-string (- (float-time) + topic-time)))))) + +(circe-set-display-handler "AUTHENTICATE" 'circe-display-ignore) +(circe-set-display-handler "CAP" 'circe-display-ignore) +(circe-set-display-handler "conn.connected" 'circe-display-ignore) +(circe-set-display-handler "conn.disconnected" 'circe-display-ignore) + +(circe-set-display-handler "irc.ctcp" 'circe-display-ignore) +(circe-set-display-handler "irc.ctcpreply" 'circe-display-ignore) + +(circe-set-display-handler "irc.ctcp.ACTION" 'circe-display-ctcp-action) +(defun circe-display-ctcp-action (nick userhost _command target text) + "Show an ACTION." + (cond + ;; Query + ((circe-server-my-nick-p target) + (let ((query-buffer (circe-query-auto-query-buffer nick))) + (with-current-buffer (or query-buffer + (circe-server-last-active-buffer)) + (circe-display (if query-buffer + 'circe-format-action + 'circe-format-message-action) + :nick nick + :userhost (or userhost "server") + :body text)))) + ;; Channel + (t + (with-current-buffer (circe-server-get-or-create-chat-buffer + target 'circe-channel-mode) + (circe-lurker-display-active nick userhost) + (circe-display 'circe-format-action + :nick nick + :userhost (or userhost "server") + :body text))))) + +(circe-set-display-handler "irc.ctcp.CLIENTINFO" 'circe-display-ctcp) + +(circe-set-display-handler "irc.ctcp.PING" 'circe-display-ctcp-ping) +(defun circe-display-ctcp-ping (nick userhost _command target text) + "Show a CTCP PING request." + (with-current-buffer (circe-server-last-active-buffer) + (circe-display 'circe-format-server-ctcp-ping + :nick nick + :userhost (or userhost "server") + :target target + :body (or text "") + :ago (let ((time (when text + (string-to-number text)))) + (if time + (format "%.2f seconds" (- (float-time) time)) + "unknown seconds"))))) + +(circe-set-display-handler "irc.ctcpreply.PING" 'circe-display-ctcp-ping-reply) +(defun circe-display-ctcp-ping-reply (nick userhost _command target text) + "Show a CTCP PING reply." + (with-current-buffer (circe-server-last-active-buffer) + (circe-display 'circe-format-server-ctcp-ping-reply + :nick nick + :userhost (or userhost "server") + :target target + :body text + :ago (let ((time (string-to-number text))) + (if time + (format "%.2f seconds" (- (float-time) time)) + "unknown seconds"))))) + +(circe-set-display-handler "irc.ctcp.SOURCE" 'circe-display-ctcp) +(circe-set-display-handler "irc.ctcp.TIME" 'circe-display-ctcp) +(circe-set-display-handler "irc.ctcp.VERSION" 'circe-display-ctcp) +(defun circe-display-ctcp (nick userhost command target text) + "Show a CTCP request that does not require special handling." + (with-current-buffer (circe-server-last-active-buffer) + (circe-display 'circe-format-server-ctcp + :nick nick + :userhost (or userhost "server") + :target target + :command (substring command 9) + :body (or text "")))) + +(circe-set-display-handler "irc.registered" 'circe-display-ignore) + +(circe-set-display-handler "JOIN" 'circe-display-JOIN) +(defun circe-display-JOIN (nick userhost _command channel + &optional accountname realname) + "Show a JOIN message. + +The command receives an extra argument, the account name, on some +IRC servers." + (let* ((accountname (if (equal accountname "*") + "(unauthenticated)" + accountname)) + (userinfo (if accountname + (format "%s, %s: %s" userhost accountname realname) + userhost)) + (split (circe--netsplit-join nick))) + ;; First, update the channel + (with-current-buffer (circe-server-get-or-create-chat-buffer + channel 'circe-channel-mode) + (cond + (split + (let ((split-time (cadr split))) + (when (< (+ split-time circe-netsplit-delay) + (float-time)) + (circe-display 'circe-format-server-netmerge + :split (car split) + :time (cadr split) + :date (current-time-string + (seconds-to-time (cadr split))) + :ago (circe-duration-string + (- (float-time) (cadr split))))))) + ((and (circe-reduce-lurker-spam) + (circe-lurker-rejoin-p nick circe-chat-target)) + (let* ((channel (irc-connection-channel (circe-server-process) + circe-chat-target)) + (user (when channel + (irc-channel-recent-user channel nick))) + (departed (when user + (irc-user-part-time user)))) + (circe-display 'circe-format-server-rejoin + :nick nick + :userhost (or userhost "server") + :accountname accountname + :realname realname + :userinfo userinfo + :departuretime departed + :departuredelta (circe-duration-string + (- (float-time) + departed))))) + ((not (circe-reduce-lurker-spam)) + (circe-display 'circe-format-server-join + :nick nick + :userhost (or userhost "server") + :accountname accountname + :realname realname + :userinfo userinfo + :channel circe-chat-target)))) + ;; Next, a possible query buffer. We do this even when the message + ;; should be ignored by a netsplit, since this can't flood. + (let ((buf (circe-server-get-chat-buffer nick))) + (when buf + (with-current-buffer buf + (circe-display 'circe-format-server-join-in-channel + :nick nick + :userhost (or userhost "server") + :accountname accountname + :realname realname + :userinfo userinfo + :channel circe-chat-target)))))) + +(circe-set-display-handler "MODE" 'circe-display-MODE) +(defun circe-display-MODE (setter userhost _command target &rest modes) + "Show a MODE message." + (with-current-buffer (or (circe-server-get-chat-buffer target) + (circe-server-last-active-buffer)) + (circe-display 'circe-format-server-mode-change + :setter setter + :userhost (or userhost "server") + :target target + :change (mapconcat #'identity modes " ")))) + +(circe-set-display-handler "NICK" 'circe-display-NICK) +(defun circe-display-NICK (old-nick userhost _command new-nick) + "Show a nick change." + (if (circe-server-my-nick-p new-nick) + (dolist (buf (cons (or circe-server-buffer + (current-buffer)) + (circe-server-chat-buffers))) + (with-current-buffer buf + (circe-display 'circe-format-server-nick-change-self + :old-nick old-nick + :userhost (or userhost "server") + :new-nick new-nick))) + (let ((query-buffer (circe-server-get-chat-buffer old-nick))) + (when query-buffer + (with-current-buffer query-buffer + (circe-server-rename-chat-buffer old-nick new-nick) + (circe-display 'circe-format-server-nick-change + :old-nick old-nick + :new-nick new-nick + :userhost (or userhost "server"))))) + (dolist (buf (circe-user-channels new-nick)) + (with-current-buffer buf + (cond + ((and (circe-reduce-lurker-spam) + (circe-lurker-p new-nick)) + nil) + ((circe-channel-user-nick-regain-p old-nick new-nick) + (circe-display 'circe-format-server-nick-regain + :old-nick old-nick + :new-nick new-nick + :userhost (or userhost "server"))) + (t + (circe-display 'circe-format-server-nick-change + :old-nick old-nick + :new-nick new-nick + :userhost (or userhost "server")))))))) + +(circe-set-display-handler "nickserv.identified" 'circe-display-ignore) + +;; NOTICE is also used to encode CTCP replies. irc.el will send +;; irc.notice events for NOTICEs without CTCP replies, so we show +;; that, not the raw notice. +(circe-set-display-handler "NOTICE" 'circe-display-ignore) +(circe-set-display-handler "irc.notice" 'circe-display-NOTICE) +(defun circe-display-NOTICE (nick userhost _command target text) + "Show a NOTICE message." + (cond + ((not userhost) + (with-current-buffer (circe-server-last-active-buffer) + (circe-display 'circe-format-server-notice + :server nick + :body text))) + ((circe-server-my-nick-p target) + (with-current-buffer (or (circe-server-get-chat-buffer nick) + (circe-server-last-active-buffer)) + (circe-display 'circe-format-notice + :nick nick + :userhost (or userhost "server") + :body text))) + (t + (with-current-buffer (or (circe-server-get-chat-buffer target) + (circe-server-last-active-buffer)) + (circe-display 'circe-format-notice + :nick nick + :userhost (or userhost "server") + :body text))))) + +(circe-set-display-handler "PART" 'circe-display-PART) +(defun circe-display-PART (nick userhost _command channel &optional reason) + "Show a PART message." + (with-current-buffer (or (circe-server-get-chat-buffer channel) + (circe-server-last-active-buffer)) + (when (or (not circe-chat-target) + (not (circe-lurker-p nick))) + (circe-display 'circe-format-server-part + :nick nick + :userhost (or userhost "server") + :channel channel + :reason (or reason "[No reason given]"))))) + +(circe-set-display-handler "PING" 'circe-display-ignore) +(circe-set-display-handler "PONG" 'circe-display-ignore) + +;; PRIVMSG is also used to encode CTCP requests. irc.el will send +;; irc.message events for PRIVMSGs without CTCP messages, so we show +;; that, not the raw message. +(circe-set-display-handler "PRIVMSG" 'circe-display-ignore) +(circe-set-display-handler "irc.message" 'circe-display-PRIVMSG) +(defun circe-display-PRIVMSG (nick userhost _command target text) + "Show a PRIVMSG message." + (cond + ((circe-server-my-nick-p target) + (let ((buf (circe-query-auto-query-buffer nick))) + (if buf + (with-current-buffer buf + (circe-display 'circe-format-say + :nick nick + :userhost (or userhost "server") + :body text)) + (with-current-buffer (circe-server-last-active-buffer) + (circe-display 'circe-format-message + :nick nick + :userhost (or userhost "server") + :body text))))) + (t + (with-current-buffer (circe-server-get-or-create-chat-buffer + target 'circe-channel-mode) + (circe-lurker-display-active nick userhost) + (circe-display 'circe-format-say + :nick nick + :userhost (or userhost "server") + :body text))))) + +(circe-set-display-handler "TOPIC" 'circe-display-topic) +(defun circe-display-topic (nick userhost _command channel new-topic) + "Show a TOPIC change." + (with-current-buffer (circe-server-get-or-create-chat-buffer + channel 'circe-channel-mode) + (let* ((channel-obj (irc-connection-channel (circe-server-process) + channel)) + (old-topic (or (when channel + (irc-channel-last-topic channel-obj)) + ""))) + (circe-display 'circe-format-server-topic + :nick nick + :userhost (or userhost "server") + :channel channel + :new-topic new-topic + :old-topic old-topic + :topic-diff (circe--topic-diff old-topic new-topic))))) + +(defun circe--topic-diff (old new) + "Return a colored topic diff between OLD and NEW." + (mapconcat (lambda (elt) + (cond + ((eq '+ (car elt)) + (let ((s (cadr elt))) + (add-face-text-property 0 (length s) + 'circe-topic-diff-new-face nil s) + s)) + ((eq '- (car elt)) + (let ((s (cadr elt))) + (add-face-text-property 0 (length s) + 'circe-topic-diff-removed-face nil s) + s)) + (t + (cadr elt)))) + (lcs-unified-diff (circe--topic-diff-split old) + (circe--topic-diff-split new) + 'string=) + "")) + +(defun circe--topic-diff-split (str) + "Split STR into a list of components. +The list consists of words and spaces." + (let ((lis nil)) + (with-temp-buffer + (insert str) + (goto-char (point-min)) + (while (< (point) + (point-max)) + (if (or (looking-at "\\w+\\W*") + (looking-at ".\\s-*")) + (progn + (setq lis (cons (match-string 0) + lis)) + (replace-match "")) + (error "Can't happen")))) + (nreverse lis))) + +(circe-set-display-handler "channel.quit" 'circe-display-channel-quit) +(defun circe-display-channel-quit (nick userhost _command channel + &optional reason) + "Show a QUIT message." + (let ((split (circe--netsplit-quit reason nick))) + (with-current-buffer (circe-server-get-or-create-chat-buffer + channel 'circe-channel-mode) + (cond + (split + (when (< (+ split circe-netsplit-delay) + (float-time)) + (circe-display 'circe-format-server-netsplit + :split reason))) + ((not (circe-lurker-p nick)) + (circe-display 'circe-format-server-quit-channel + :nick nick + :userhost (or userhost "server") + :channel channel + :reason (or reason "[no reason given]"))))))) + +(circe-set-display-handler "QUIT" 'circe-display-QUIT) +(defun circe-display-QUIT (nick userhost _command &optional reason) + "Show a QUIT message. + +Channel quits are shown already, so just show quits in queries." + (let ((buf (circe-server-get-chat-buffer nick))) + (when buf + (with-current-buffer buf + (circe-display 'circe-format-server-quit + :nick nick + :userhost (or userhost "server") + :reason (or reason "[no reason given]")))))) + +(defvar circe-netsplit-list nil + "A list of recorded netsplits. +Every item is a list with four elements: +- The quit message for this split. +- The time when last we heard about a join in this split +- The time when last we heard about a quit in this split +- A hash table noting which nicks did leave") +(make-variable-buffer-local 'circe-netsplit-list) + +(defun circe--netsplit-join (nick) + "Search for NICK in the netsplit lists. +This either returns a pair whose car is the quit message of this +split, and the cadr the time we last heard anything of the split +of that user. If the NICK isn't split, this returns nil." + (with-circe-server-buffer + (catch 'return + (dolist (entry circe-netsplit-list) + (let ((table (nth 3 entry))) + (when (gethash nick table) + (let ((name (nth 0 entry)) + (time (nth 1 entry))) + (remhash nick table) + (when (= 0 (hash-table-count table)) + (setq circe-netsplit-list + (delq entry circe-netsplit-list))) + (setcar (cdr entry) + (float-time)) + (throw 'return (list name time)))))) + nil))) + +(defun circe--netsplit-quit (reason nick) + "If REASON indicates a netsplit, mark NICK as splitted. +This either returns the time when last we heard about this split, +or nil when this isn't a split." + (when (circe--netsplit-reason-p reason) + (with-circe-server-buffer + (let ((entry (assoc reason circe-netsplit-list))) + (if entry + (let ((time (nth 2 entry)) + (table (nth 3 entry))) + (setcar (cddr entry) + (float-time)) + (puthash nick nick table) + time) + ;; New split! + (let ((table (make-hash-table :test 'equal))) + (puthash nick nick table) + (setq circe-netsplit-list + (cons (list reason 0 (float-time) table) + circe-netsplit-list)) + 0)))))) + +(defun circe--netsplit-reason-p (reason) + "Return non-nil if REASON is the quit message of a netsplit. +This is true when it contains exactly two hosts, with a single +space in between them. The hosts must include at least one dot, +and must not include colons or slashes (else they might be +URLs). (Thanks to irssi for this criteria list)" + (if (string-match "^[^ :/]+\\.[^ :/]* [^ :/]+\\.[^ :/]*$" + reason) + t + nil)) + +(let ((simple-format-specifiers + '(("INVITE" active "Invite: {origin} invites you to {1}") + ("KICK" 0 "Kick: {1} kicked by {origin}: {2}") + ("ERROR" active "Error: {0-}") + ("001" server "{1}") + ("002" server "{1}") + ("003" server "{1}") + ("004" server "{1-}") + ("005" server "{1-}") + ;; IRCnet: * Please wait while we process your connection. + ("020" server "{0-}") + ;; IRCnet + ("042" server "Your unique ID is {1}") + ("200" active "{1-}") + ("201" active "{1-}") + ("203" active "{1-}") + ("204" active "{1-}") + ("205" active "{1-}") + ("206" active "{1-}") + ("207" active "{1-}") + ("208" active "{1-}") + ("209" active "{1-}") + ("211" active "{1-}") + ("212" active "{1-}") + ("219" active "{1-}") + ("221" active "User mode: {1-}") + ("234" active "Service: {1-}") + ("235" active "{1-}") + ("242" active "{1}") + ("243" active "{1-}") + ("250" server "{1}") + ("251" server "{1}") + ("252" server "{1-}") + ("253" server "{1-}") + ("254" server "{1-}") + ("255" server "{1}") + ("256" active "{1-}") + ("257" active "{1}") + ("258" active "{1}") + ("259" active "{1}") + ("261" active "{1-}") + ("262" active "{1-}") + ("263" active "{1-}") + ("265" server "{1-}") + ("266" server "{1-}") + ;; This is returned on both WHOIS and PRIVMSG. It + ;; should go to the active window for the former, and + ;; the query window for the latter. Oh well. + ("301" active "User away: {1}") + ("302" active "User hosts: {1}") + ("303" active "Users online: {1}") + ("305" active "{1}") + ("306" active "{1}") + ("307" active "{1-}") + ;; Coldfront: 310 <nick> is available for help. + ("310" active "{1-}") + ("311" active "{1} is {2}@{3} ({5})") + ("312" active "{1} is on {2} ({3})") + ("313" active "{1} {2}") + ("314" active "{1} was {2}@{3} ({5})") + ("315" active "{2}") + ("318" active "{2}") + ("319" active "{1} is on {2}") + ("320" active "{1-}") + ("322" active "{1-}") + ("323" active "{1-}") + ("324" 1 "Channel mode for {1}: {2-}") + ("325" 1 "Unique operator on {1} is {2}") + ("328" 1 "Channel homepage for {1}: {2-}") + ("330" active "{1} is logged in as {2}") + ("331" 1 "No topic for {1} set") + ("332" 1 "Topic for {1}: {2}") + ("341" active "Inviting {1} to {2}") + ("346" 1 "Invite mask: {2}") + ("347" 1 "{2}") + ("348" 1 "Except mask: {2}") + ("349" 1 "{2}") + ("351" active "{1-}") + ("352" active "{5} ({2}@{3}) in {1} on {4}: {6-}") + ("353" 2 "Names: {3}") + ("364" active "{1-}") + ("365" active "{1-}") + ("366" 1 "{2}") + ("367" 1 "Ban mask: {2}") + ("368" 1 "{2}") + ("369" active "{1} {2}") + ("371" active "{1}") + ("372" server "{1}") + ("374" active "{1}") + ("375" server "{1}") + ("376" server "{1}") + ("378" active "{1-}") + ("381" active "{1}") + ("382" active "{1-}") + ("391" active "Time on {1}: {2}") + ("401" active "No such nick: {1}") + ("402" active "No such server: {1}") + ("403" active "No such channel: {1}") + ("404" 1 "Can not send to channel {1}") + ("405" active "Can not join {1}: {2}") + ("406" active "{1-}") + ("407" active "{1-}") + ("408" active "No such service: {1}") + ("422" active "{1}") + ("432" active "Erroneous nick name: {1}") + ("433" active "Nick name in use: {1}") + ("437" active "Nick/channel is temporarily unavailable: {1}") + ("441" 2 "User not on channel: {1}") + ("442" active "You are not on {1}") + ("443" 2 "User {1} is already on channel {2}") + ;; Coldfront: 451 * :You have not registered + ("451" active "{1-}") + ("467" 1 "{2}") + ("470" 1 "{1} made you join {2}: {3-}") + ("471" 1 "{2}") + ("472" active "{1-}") + ("473" active "{1-}") + ("474" active "{1-}") + ("475" active "{1-}") + ("476" active "{1-}") + ("477" active "{1-}") + ("481" 1 "{2-}") + ("484" active "{1-}") + ;; Coldfront: 671 <nick> is using a Secure Connection + ("671" active "{1-}") + ("728" 1 "Quiet mask: {3}") + ("729" 1 "{3-}") + ;; Freenode SASL auth + ("900" active "SASL: {3-}") + ("903" active "{1-}")))) + (dolist (fmt simple-format-specifiers) + (circe-set-display-handler (car fmt) (cdr fmt)))) + +(defun circe-set-message-target (command target) + "Set the target of COMMAND to TARGET. + +This can be used to change format-based display handlers more +easily." + (let ((handler (circe-get-display-handler command))) + (when (not (consp handler)) + (error "Handler of command %s is not a list" command)) + (setcar handler target))) + +;;;;;;;;;;;;;;;;;;;;;;;; +;;; Helper Functions ;;; +;;;;;;;;;;;;;;;;;;;;;;;; + +(defun circe--list-drop-right (list pattern) + "Drop elements from the right of LIST that match PATTERN. + +LIST should be a list of strings, and PATTERN is used as a +regular expression." + (let ((list (reverse list))) + (while (and list + (string-match pattern (car list))) + (setq list (cdr list))) + (nreverse list))) + +(defun circe--nick-next (oldnick) + "Return a new nick to try for OLDNICK." + (cond + ;; If the nick ends with -+, replace those with _ + ((string-match "^\\(.*[^-]\\)\\(-+\\)$" oldnick) + (concat (match-string 1 oldnick) + (make-string (- (match-end 2) + (match-beginning 2)) + ?_))) + ;; If the nick is 9 chars long, take prefix and rotate. + ((>= (length oldnick) + 9) + (when (string-match "^\\(.*[^-_]\\)[-_]*$" oldnick) + (let ((nick (match-string 1 oldnick))) + (concat (substring nick 1) + (string (aref nick 0)))))) + ;; If the nick ends with _+ replace those with - and add one + ((string-match "^\\(.*[^_]\\)\\(_+\\)$" oldnick) + (concat (match-string 1 oldnick) + (make-string (- (match-end 2) + (match-beginning 2)) + ?-) + "-")) + ;; Else, just append - + (t + (concat oldnick "-")))) + +(defun circe-duration-string (duration) + "Return a description of a DURATION in seconds." + (let ((parts `((,(* 12 30 24 60 60) "year") + (,(* 30 24 60 60) "month") + (,(* 24 60 60) "day") + (,(* 60 60) "hour") + (60 "minute") + (1 "second"))) + (duration (round duration)) + (result nil)) + (dolist (part parts) + (let* ((seconds-per-part (car part)) + (description (cadr part)) + (count (/ duration seconds-per-part))) + (when (not (zerop count)) + (setq result (cons (format "%d %s%s" + count description + (if (= count 1) "" "s")) + result))) + (setq duration (- duration (* count seconds-per-part))))) + (if result + (mapconcat #'identity + (nreverse result) + " ") + "a moment"))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; Deprecated functions and variables + +(define-obsolete-function-alias 'circe-server-nick 'circe-nick + "Circe 2.0") + +(define-obsolete-function-alias 'circe-server-message + 'circe-display-server-message + "Circe 2.0") + +(define-obsolete-variable-alias 'circe-networks 'circe-network-defaults + "Circe 2.0") + +(define-obsolete-variable-alias 'circe-server-name 'circe-host + "Circe 2.0") + +(define-obsolete-variable-alias 'circe-server-service 'circe-port + "Circe 2.0") + +(define-obsolete-variable-alias 'circe-server-network 'circe-network + "Circe 2.0") + +(define-obsolete-variable-alias 'circe-server-ip-family 'circe-ip-family + "Circe 2.0") + +(define-obsolete-variable-alias 'circe-server-nick 'circe-nick + "Circe 2.0") + +(define-obsolete-variable-alias 'circe-server-user 'circe-user + "Circe 2.0") + +(define-obsolete-variable-alias 'circe-server-pass 'circe-pass + "Circe 2.0") + +(define-obsolete-variable-alias 'circe-server-realname 'circe-realname + "Circe 2.0") + +(define-obsolete-variable-alias 'circe-server-use-tls 'circe-use-tls + "Circe 2.0") + +(define-obsolete-variable-alias 'circe-server-auto-join-channels + 'circe-channels + "Circe 2.0") + +(provide 'circe) +;;; circe.el ends here |