Mercurial > emacs
diff lisp/erc/erc-track.el @ 68451:fc745b05e928
Revision: emacs@sv.gnu.org/emacs--devo--0--patch-22
Creator: Michael Olson <mwolson@gnu.org>
Install ERC.
author | Miles Bader <miles@gnu.org> |
---|---|
date | Sun, 29 Jan 2006 13:08:58 +0000 |
parents | |
children | 7010bb070445 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lisp/erc/erc-track.el Sun Jan 29 13:08:58 2006 +0000 @@ -0,0 +1,839 @@ +;;; erc-track.el --- Track modified channel buffers + +;; Copyright (C) 2002, 2003, 2004, 2005 Free Software Foundation, Inc. + +;; Author: Mario Lang <mlang@delysid.org> +;; Keywords: comm, faces +;; URL: http://www.emacswiki.org/cgi-bin/wiki.pl?ErcChannelTracking + +;; This file is part of GNU Emacs. + +;; GNU Emacs is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation; either version 2, or (at your option) +;; any later version. + +;; GNU Emacs is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with GNU Emacs; see the file COPYING. If not, write to the +;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, +;; Boston, MA 02110-1301, USA. + +;;; Commentary: + +;; Highlights keywords and pals (friends), and hides or highlights fools +;; (using a dark color). Add to your ~/.emacs: + +;; (require 'erc-track) +;; (erc-track-mode 1) + +;; Todo: +;; * Add extensibility so that custom functions can track +;; custom modification types. + +(eval-when-compile (require 'cl)) +(require 'erc) +(require 'erc-compat) +(require 'erc-match) + +;;; Code: + +(defgroup erc-track nil + "Track active buffers and show activity in the modeline." + :group 'erc) + +(defcustom erc-track-visibility t + "Where do we look for buffers to determine their visibility? +The value of this variable determines, when a buffer is considered +visible or invisible. New messages in invisible buffers are tracked, +while switching to visible buffers when they are tracked removes them +from the list. See also `erc-track-when-inactive-mode'. + +Possible values are: + +t - all frames +visible - all visible frames +nil - only the selected frame +selected-visible - only the selected frame if it is visible + +Activity means that there was no user input in the last 10 seconds." + :group 'erc-track + :type '(choice (const :tag "All frames" t) + (const :tag "All visible frames" visible) + (const :tag "Only the selected frame" nil) + (const :tag "Only the selected frame if it was active" + active))) + +(defcustom erc-track-exclude nil + "A list targets (channel names or query targets) which should not be tracked." + :group 'erc-track + :type '(repeat string)) + +(defcustom erc-track-exclude-types '("NICK") + "*List of message types to be ignored. +This list could look like '(\"JOIN\" \"PART\")." + :group 'erc-track + :type 'erc-message-type) + +(defcustom erc-track-exclude-server-buffer nil + "*If true, don't perform tracking on the server buffer; this is +useful for excluding all the things like MOTDs from the server and +other miscellaneous functions." + :group 'erc-track + :type 'boolean) + +(defcustom erc-track-shorten-start 1 + "This number specifies the minimum number of characters a channel name in +the mode-line should be reduced to." + :group 'erc-track + :type 'number) + +(defcustom erc-track-shorten-cutoff 4 + "All channel names longer than this value will be shortened." + :group 'erc-track + :type 'number) + +(defcustom erc-track-shorten-aggressively nil + "*If non-nil, channel names will be shortened more aggressively. +Usually, names are not shortened if this will save only one character. +Example: If there are two channels, #linux-de and #linux-fr, then +normally these will not be shortened. When shortening aggressively, +however, these will be shortened to #linux-d and #linux-f. + +If this variable is set to `max', then channel names will be shortened +to the max. Usually, shortened channel names will remain unique for a +given set of existing channels. When shortening to the max, the shortened +channel names will be unique for the set of active channels only. +Example: If there are tow active channels #emacs and #vi, and two inactive +channels #electronica and #folk, then usually the active channels are +shortened to #em and #v. When shortening to the max, however, #emacs is +not compared to #electronica -- only to #vi, therefore it can be shortened +even more and the result is #e and #v. + +This setting is used by `erc-track-shorten-names'." + :group 'erc-track + :type '(choice (const :tag "No" nil) + (const :tag "Yes" t) + (const :tag "Max" max))) + +(defcustom erc-track-shorten-function 'erc-track-shorten-names + "*This function will be used to reduce the channel names before display. +It takes one argument, CHANNEL-NAMES which is a list of strings. +It should return a list of strings of the same number of elements. +If nil instead of a function, shortening is disabled." + :group 'erc-track + :type '(choice (const :tag "Disabled") + function)) + +(defcustom erc-track-use-faces t + "*Use faces in the mode-line. +The faces used are the same as used for text in the buffers. +\(e.g. `erc-pal-face' is used if a pal sent a message to that channel.)" + :group 'erc-track + :type 'boolean) + +(defcustom erc-track-faces-priority-list + '(erc-error-face erc-current-nick-face erc-keyword-face erc-pal-face + erc-nick-msg-face erc-direct-msg-face erc-button erc-dangerous-host-face + erc-default-face erc-action-face erc-nick-default-face erc-fool-face + erc-notice-face erc-input-face erc-prompt-face) + "A list of faces used to highlight active buffer names in the modeline. +If a message contains one of the faces in this list, the buffer name will +be highlighted using that face. The first matching face is used." + :group 'erc-track + :type '(repeat face)) + +(defcustom erc-track-priority-faces-only nil + "Only track text highlighted with a priority face. +If you would like to ignore changes in certain channels where there +are no faces corresponding to your `erc-track-faces-priority-list', set +this variable. You can set a list of channel name strings, so those +will be ignored while all other channels will be tracked as normal. +Other options are 'all, to apply this to all channels or nil, to disable +this feature. +Note: If you have a lot of faces listed in `erc-track-faces-priority-list', +setting this variable might not be very useful." + :group 'erc-track + :type '(choice (const nil) + (repeat string) + (const all))) + +(defcustom erc-track-position-in-mode-line 'before-modes + "Where to show modified channel information in the mode-line. + +Setting this variable only has effects in GNU Emacs versions above 21.3. + +Choices are: +'before-modes - add to the beginning of `mode-line-modes' +'after-modes - add to the end of `mode-line-modes' + +Any other value means add to the end of `global-mode-string'." + :group 'erc-track + :type '(choice (const :tag "Just before mode information" before-modes) + (const :tag "Just after mode information" after-modes) + (const :tag "After all other information" nil)) + :set (lambda (sym val) + (set sym val) + (when (and (boundp 'erc-track-mode) + erc-track-mode) + (erc-track-remove-from-mode-line) + (erc-track-add-to-mode-line val)))) + +(defun erc-modified-channels-object (strings) + "Generate a new `erc-modified-channels-object' based on STRINGS. +If STRINGS is nil, we initialize `erc-modified-channels-object' to +an appropriate initial value for this flavor of Emacs." + (if strings + (if (featurep 'xemacs) + (let ((e-m-c-s '("["))) + (push (cons (extent-at 0 (car strings)) (car strings)) + e-m-c-s) + (dolist (string (cdr strings)) + (push "," e-m-c-s) + (push (cons (extent-at 0 string) string) + e-m-c-s)) + (push "] " e-m-c-s) + (reverse e-m-c-s)) + (concat (if (eq erc-track-position-in-mode-line 'after-modes) + "[" " [") + (mapconcat 'identity (nreverse strings) ",") + (if (eq erc-track-position-in-mode-line 'before-modes) + "] " "]"))) + (if (featurep 'xemacs) '() ""))) + +(defvar erc-modified-channels-object (erc-modified-channels-object nil) + "Internal object used for displaying modified channels in the mode line.") + +(put 'erc-modified-channels-object 'risky-local-variable t); allow properties + +(defvar erc-modified-channels-alist nil + "An ALIST used for tracking channel modification activity. +Each element looks like (BUFFER COUNT FACE) where BUFFER is a buffer +object of the channel the entry corresponds to, COUNT is a number +indicating how often activity was noticed, and FACE is the face to use +when displaying the buffer's name. See `erc-track-faces-priority-list', +and `erc-track-showcount'. + +Entries in this list should only happen for buffers where activity occurred +while the buffer was not visible.") + +(defcustom erc-track-showcount nil + "If non-nil, count of unseen messages will be shown for each channel." + :type 'boolean + :group 'erc-track) + +(defcustom erc-track-showcount-string ":" + "The string to display between buffer name and the count in the mode line. +The default is a colon, resulting in \"#emacs:9\"." + :type 'string + :group 'erc-track) + +(defcustom erc-track-switch-from-erc t + "If non-nil, `erc-track-switch-buffer' will return to the last non-erc buffer +when there are no more active channels." + :type 'boolean + :group 'erc-track) + +(defcustom erc-track-switch-direction 'oldest + "Direction `erc-track-switch-buffer' should switch. + + oldest - find oldest active buffer + newest - find newest active buffer + leastactive - find buffer with least unseen messages + mostactive - find buffer with most unseen messages." + :group 'erc-track + :type '(choice (const oldest) + (const newest) + (const leastactive) + (const mostactive))) + + +(defun erc-track-remove-from-mode-line () + "Remove `erc-track-modified-channels' from the mode-line" + (when (boundp 'mode-line-modes) + (setq mode-line-modes + (remove '(t erc-modified-channels-object) mode-line-modes))) + (when (consp global-mode-string) + (setq global-mode-string + (delq 'erc-modified-channels-object global-mode-string)))) + +(defun erc-track-add-to-mode-line (position) + "Add `erc-track-modified-channels' to POSITION in the mode-line. +See `erc-track-position-in-mode-line' for possible values." + ;; CVS Emacs has a new format string, and global-mode-string + ;; is very far to the right. + (cond ((and (eq position 'before-modes) + (boundp 'mode-line-modes)) + (add-to-list 'mode-line-modes + '(t erc-modified-channels-object))) + ((and (eq position 'after-modes) + (boundp 'mode-line-modes)) + (add-to-list 'mode-line-modes + '(t erc-modified-channels-object) t)) + (t + (when (not global-mode-string) + (setq global-mode-string '(""))) ; Padding for mode-line wart + (add-to-list 'global-mode-string + 'erc-modified-channels-object + t)))) + +;;; Shortening of names + +(defun erc-track-shorten-names (channel-names) + "Call `erc-unique-channel-names' with the correct parameters. +This function is a good value for `erc-track-shorten-function'. +The list of all channels is returned by `erc-all-buffer-names'. +CHANNEL-NAMES is the list of active channel names. +Only channel names longer than `erc-track-shorten-cutoff' are +actually shortened, and they are only shortened to a minimum +of `erc-track-shorten-start' characters." + (erc-unique-channel-names + (erc-all-buffer-names) + channel-names + (lambda (s) + (> (length s) erc-track-shorten-cutoff)) + erc-track-shorten-start)) + +(defvar erc-default-recipients) + +(defun erc-all-buffer-names () + "Return all channel or query buffer names. +Note that we cannot use `erc-channel-list' with a nil argument, +because that does not return query buffers." + (save-excursion + (let (result) + (dolist (buf (buffer-list)) + (set-buffer buf) + (when (or (eq major-mode 'erc-mode) (eq major-mode 'erc-dcc-chat-mode)) + (setq result (cons (buffer-name) result)))) + result))) + +(defun erc-unique-channel-names (all active &optional predicate start) + "Return a list of unique channel names. +ALL is the list of all channel and query buffer names. +ACTIVE is the list of active buffer names. +PREDICATE is a predicate that should return non-nil if a name needs + no shortening. +START is the minimum length of the name used." + (if (eq 'max erc-track-shorten-aggressively) + ;; Return the unique substrings of all active channels. + (erc-unique-substrings active predicate start) + ;; Otherwise, determine the unique substrings of all channels, and + ;; for every active channel, return the corresponding substring. + ;; Given the names of the active channels, we now need to find the + ;; corresponding short name from the list of all substrings. To + ;; avoid problems when there are two channels and one is a + ;; substring of the other (notorious examples are #hurd and + ;; #hurd-bunny), every candidate gets the longest possible + ;; substring. + (let ((all-substrings (sort + (erc-unique-substrings all predicate start) + (lambda (a b) (> (length a) (length b))))) + result) + (dolist (channel active) + (let ((substrings all-substrings) + candidate + winner) + (while (and substrings (not winner)) + (setq candidate (car substrings) + substrings (cdr substrings)) + (when (and (string= candidate + (substring channel + 0 + (min (length candidate) + (length channel)))) + (not (member candidate result))) + (setq winner candidate))) + (setq result (cons winner result)))) + (nreverse result)))) + +(defun erc-unique-substrings (strings &optional predicate start) + "Return a list of unique substrings of STRINGS." + (if (or (not (numberp start)) + (< start 0)) + (setq start 2)) + (mapcar + (lambda (str) + (let* ((others (delete str (copy-sequence strings))) + (maxlen (length str)) + (i (min start + (length str))) + candidate + done) + (if (and (functionp predicate) (not (funcall predicate str))) + ;; do not shorten if a predicate exists and it returns nil + str + ;; Start with smallest substring candidate, ie. length 1. + ;; Then check all the others and see whether any of them starts + ;; with the same substring. While there is such another + ;; element in the list, increase the length of the candidate. + (while (not done) + (if (> i maxlen) + (setq done t) + (setq candidate (substring str 0 i) + done (not (erc-unique-substring-1 candidate others)))) + (setq i (1+ i))) + (if (and (= (length candidate) (1- maxlen)) + (not erc-track-shorten-aggressively)) + str + candidate)))) + strings)) + +(defun erc-unique-substring-1 (candidate others) + "Return non-nil when any string in OTHERS starts with CANDIDATE." + (let (result other (maxlen (length candidate))) + (while (and others + (not result)) + (setq other (car others) + others (cdr others)) + (when (and (>= (length other) maxlen) + (string= candidate (substring other 0 maxlen))) + (setq result other))) + result)) + +;;; Test: + +(erc-assert + (and + ;; verify examples from the doc strings + (equal (let ((erc-track-shorten-aggressively nil)) + (erc-unique-channel-names + '("#emacs" "#vi" "#electronica" "#folk") + '("#emacs" "#vi"))) + '("#em" "#vi")) ; emacs is different from electronica + (equal (let ((erc-track-shorten-aggressively t)) + (erc-unique-channel-names + '("#emacs" "#vi" "#electronica" "#folk") + '("#emacs" "#vi"))) + '("#em" "#v")) ; vi is shortened by one letter + (equal (let ((erc-track-shorten-aggressively 'max)) + (erc-unique-channel-names + '("#emacs" "#vi" "#electronica" "#folk") + '("#emacs" "#vi"))) + '("#e" "#v")) ; emacs need not be different from electronica + (equal (let ((erc-track-shorten-aggressively nil)) + (erc-unique-channel-names + '("#linux-de" "#linux-fr") + '("#linux-de" "#linux-fr"))) + '("#linux-de" "#linux-fr")) ; shortening by one letter is too aggressive + (equal (let ((erc-track-shorten-aggressively t)) + (erc-unique-channel-names + '("#linux-de" "#linux-fr") + '("#linux-de" "#linux-fr"))) + '("#linux-d" "#linux-f")); now we want to be aggressive + ;; specific problems + (equal (let ((erc-track-shorten-aggressively nil)) + (erc-unique-channel-names + '("#dunnet" "#lisp" "#sawfish" "#fsf" "#guile" + "#testgnome" "#gnu" "#fsbot" "#hurd" "#hurd-bunny" + "#emacs") + '("#hurd-bunny" "#hurd" "#sawfish" "#lisp"))) + '("#hurd-" "#hurd" "#s" "#l")) + (equal (let ((erc-track-shorten-aggressively nil)) + (erc-unique-substrings + '("#emacs" "#vi" "#electronica" "#folk"))) + '("#em" "#vi" "#el" "#f")) + (equal (let ((erc-track-shorten-aggressively t)) + (erc-unique-substrings + '("#emacs" "#vi" "#electronica" "#folk"))) + '("#em" "#v" "#el" "#f")) + (equal (let ((erc-track-shorten-aggressively nil)) + (erc-unique-channel-names + '("#emacs" "#burse" "+linux.de" "#starwars" + "#bitlbee" "+burse" "#ratpoison") + '("+linux.de" "#starwars" "#burse"))) + '("+l" "#s" "#bu")) + (equal (let ((erc-track-shorten-aggressively nil)) + (erc-unique-channel-names + '("fsbot" "#emacs" "deego") + '("fsbot"))) + '("fs")) + (equal (let ((erc-track-shorten-aggressively nil)) + (erc-unique-channel-names + '("fsbot" "#emacs" "deego") + '("fsbot") + (lambda (s) + (> (length s) 4)) + 1)) + '("f")) + (equal (let ((erc-track-shorten-aggressively nil)) + (erc-unique-channel-names + '("fsbot" "#emacs" "deego") + '("fsbot") + (lambda (s) + (> (length s) 4)) + 2)) + '("fs")) + (let ((erc-track-shorten-aggressively nil)) + (equal (erc-unique-channel-names '("deego" "#hurd" "#hurd-bunny" "#emacs") + '("#hurd" "#hurd-bunny")) + '("#hurd" "#hurd-"))) + ;; general examples + (let ((erc-track-shorten-aggressively t)) + (and (equal (erc-unique-substring-1 "abc" '("ab" "abcd")) "abcd") + (not (erc-unique-substring-1 "a" '("xyz" "xab"))) + (equal (erc-unique-substrings '("abc" "xyz" "xab")) + '("ab" "xy" "xa")) + (equal (erc-unique-substrings '("abc" "abcdefg")) + '("abc" "abcd")))) + (let ((erc-track-shorten-aggressively nil)) + (and (equal (erc-unique-substring-1 "abc" '("ab" "abcd")) "abcd") + (not (erc-unique-substring-1 "a" '("xyz" "xab"))) + (equal (erc-unique-substrings '("abc" "xyz" "xab")) + '("abc" "xyz" "xab")) + (equal (erc-unique-substrings '("abc" "abcdefg")) + '("abc" "abcd")))))) + +;;; Module + +;;;###autoload (autoload 'erc-track-mode "erc-track" nil t) +(define-erc-module track track-modified-channels + "This mode tracks ERC channel buffers with activity." + ((erc-track-add-to-mode-line erc-track-position-in-mode-line) + (setq erc-modified-channels-object (erc-modified-channels-object nil)) + (erc-update-mode-line) + (if (featurep 'xemacs) + (defadvice switch-to-buffer (after erc-update (&rest args) activate) + (erc-modified-channels-update)) + (add-hook 'window-configuration-change-hook 'erc-modified-channels-update)) + (add-hook 'erc-insert-post-hook 'erc-track-modified-channels) + (add-hook 'erc-disconnected-hook 'erc-modified-channels-update)) + ((erc-track-remove-from-mode-line) + (if (featurep 'xemacs) + (ad-disable-advice 'switch-to-buffer 'after 'erc-update) + (remove-hook 'window-configuration-change-hook + 'erc-modified-channels-update)) + (remove-hook 'erc-disconnected-hook 'erc-modified-channels-update) + (remove-hook 'erc-insert-post-hook 'erc-track-modified-channels))) + +;;;###autoload (autoload 'erc-track-when-inactive-mode "erc-track" nil t) +(define-erc-module track-when-inactive nil + "This mode enables channel tracking even for visible buffers, +if you are inactivity." + ((if (featurep 'xemacs) + (defadvice switch-to-buffer (after erc-update-when-inactive (&rest args) activate) + (erc-user-is-active)) + (add-hook 'window-configuration-change-hook 'erc-user-is-active)) + (add-hook 'erc-send-completed-hook 'erc-user-is-active) + (add-hook 'erc-server-001-functions 'erc-user-is-active)) + ((erc-track-remove-from-mode-line) + (if (featurep 'xemacs) + (ad-disable-advice 'switch-to-buffer 'after 'erc-update-when-inactive) + (remove-hook 'window-configuration-change-hook 'erc-user-is-active)) + (remove-hook 'erc-send-completed-hook 'erc-user-is-active) + (remove-hook 'erc-server-001-functions 'erc-user-is-active) + (remove-hook 'erc-timer-hook 'erc-user-is-active))) + +;;; Visibility + +(defvar erc-buffer-activity nil + "Last time the user sent something.") + +(defvar erc-buffer-activity-timeout 10 + "How many seconds of inactivity by the user +to consider when `erc-track-visibility' is set to +only consider active buffers visible.") + +(defun erc-user-is-active (&rest ignore) + "Set `erc-buffer-activity'." + (setq erc-buffer-activity (erc-current-time)) + (erc-track-modified-channels)) + +(defun erc-buffer-visible (buffer) + "Return non-nil when the buffer is visible." + (if erc-track-when-inactive-mode + (when erc-buffer-activity; could be nil + (and (get-buffer-window buffer erc-track-visibility) + (<= (erc-time-diff erc-buffer-activity (erc-current-time)) + erc-buffer-activity-timeout))) + (get-buffer-window buffer erc-track-visibility))) + +;;; Tracking the channel modifications + +(defvar erc-modified-channels-update-inside nil + "Variable to prevent running `erc-modified-channels-update' multiple +times. Without it, you cannot debug `erc-modified-channels-display', +because the debugger also cases changes to the window-configuration.") + +(defun erc-modified-channels-update (&rest args) + "This function updates the information in `erc-modified-channels-alist' +according to buffer visibility. It calls +`erc-modified-channels-display' at the end. This should usually be +called via `window-configuration-change-hook'. +ARGS are ignored." + (interactive) + (unless erc-modified-channels-update-inside + (let ((erc-modified-channels-update-inside t)) + (mapcar (lambda (elt) + (let ((buffer (car elt))) + (when (or (not (bufferp buffer)) + (not (buffer-live-p buffer)) + (erc-buffer-visible buffer) + (not (with-current-buffer buffer + erc-server-connected))) + (erc-modified-channels-remove-buffer buffer)))) + erc-modified-channels-alist) + (erc-modified-channels-display) + (force-mode-line-update t)))) + +(defun erc-make-mode-line-buffer-name (string buffer &optional faces count) + "Return STRING as a button that switches to BUFFER when clicked. +If FACES are provided, color STRING with them." + ;; We define a new sparse keymap every time, because 1. this data + ;; structure is very small, the alternative would require us to + ;; defvar a keymap, 2. the user is not interested in customizing it + ;; (really?), 3. the defun needs to switch to BUFFER, so we would + ;; need to save that value somewhere. + (let ((map (make-sparse-keymap)) + (name (if erc-track-showcount + (concat string + erc-track-showcount-string + (int-to-string count)) + (copy-sequence string)))) + (define-key map (vector 'mode-line 'mouse-2) + `(lambda (e) + (interactive "e") + (save-selected-window + (select-window + (posn-window (event-start e))) + (switch-to-buffer ,buffer)))) + (define-key map (vector 'mode-line 'mouse-3) + `(lambda (e) + (interactive "e") + (save-selected-window + (select-window + (posn-window (event-start e))) + (switch-to-buffer-other-window ,buffer)))) + (put-text-property 0 (length name) 'local-map map name) + (when (and faces erc-track-use-faces) + (put-text-property 0 (length name) 'face faces name)) + name)) + +(defun erc-modified-channels-display () + "Set `erc-modified-channels-object' +according to `erc-modified-channels-alist'. +Use `erc-make-mode-line-buffer-name' to create buttons." + (if (or + (eq 'mostactive erc-track-switch-direction) + (eq 'leastactive erc-track-switch-direction)) + (erc-track-sort-by-activest)) + (if (null erc-modified-channels-alist) + (setq erc-modified-channels-object (erc-modified-channels-object nil)) + ;; erc-modified-channels-alist contains all the data we need. To + ;; better understand what is going on, we split things up into + ;; four lists: BUFFERS, COUNTS, SHORT-NAMES, and FACES. These + ;; four lists we use to create a new + ;; `erc-modified-channels-object' using + ;; `erc-make-mode-line-buffer-name'. + (let* ((buffers (mapcar 'car erc-modified-channels-alist)) + (counts (mapcar 'cadr erc-modified-channels-alist)) + (faces (mapcar 'cddr erc-modified-channels-alist)) + (long-names (mapcar #'(lambda (buf) + (or (buffer-name buf) + "")) + buffers)) + (short-names (if (functionp erc-track-shorten-function) + (funcall erc-track-shorten-function + long-names) + long-names)) + strings) + (while buffers + (when (car short-names) + (setq strings (cons (erc-make-mode-line-buffer-name + (car short-names) + (car buffers) + (car faces) + (car counts)) + strings))) + (setq short-names (cdr short-names) + buffers (cdr buffers) + counts (cdr counts) + faces (cdr faces))) + (when (featurep 'xemacs) + (erc-modified-channels-object nil)) + (setq erc-modified-channels-object + (erc-modified-channels-object strings))))) + +(defun erc-modified-channels-remove-buffer (buffer) + "Remove BUFFER from `erc-modified-channels-alist'." + (interactive "bBuffer: ") + (setq erc-modified-channels-alist + (delete (assq buffer erc-modified-channels-alist) + erc-modified-channels-alist)) + (when (interactive-p) + (erc-modified-channels-display))) + +(defun erc-track-find-face (faces) + "Return the face to use in the modeline from the faces in FACES. +If `erc-track-faces-priority-list' is set, the one from FACES who is +first in that list will be used." + (let ((candidates erc-track-faces-priority-list) + candidate face) + (while (and candidates (not face)) + (setq candidate (car candidates) + candidates (cdr candidates)) + (when (memq candidate faces) + (setq face candidate))) + face)) + +(defun erc-track-modified-channels () + "Hook function for `erc-insert-post-hook' to check if the current +buffer should be added to the modeline as a hidden, modified +channel. Assumes it will only be called when current-buffer +is in `erc-mode'." + (let ((this-channel (or (erc-default-target) + (buffer-name (current-buffer))))) + (if (and (not (erc-buffer-visible (current-buffer))) + (not (member this-channel erc-track-exclude)) + (not (and erc-track-exclude-server-buffer + (string= this-channel + (buffer-name (erc-server-buffer))))) + (not (erc-message-type-member + (or (erc-find-parsed-property) + (point-min)) + erc-track-exclude-types))) + ;; If the active buffer is not visible (not shown in a + ;; window), and not to be excluded, determine the kinds of + ;; faces used in the current message, and unless the user + ;; wants to ignore changes in certain channels where there + ;; are no faces corresponding to `erc-track-faces-priority-list', + ;; and the faces in the current message are found in said + ;; priority list, add the buffer to the erc-modified-channels-alist, + ;; if it is not already there. If the buffer is already on the list + ;; (in the car), change its face attribute (in the cddr) if + ;; necessary. See `erc-modified-channels-alist' for the + ;; exact data structure used. + (let ((faces (erc-faces-in (buffer-string)))) + (unless (and + (or (eq erc-track-priority-faces-only 'all) + (member this-channel erc-track-priority-faces-only)) + (not (catch 'found + (dolist (f faces) + (when (member f erc-track-faces-priority-list) + (throw 'found t)))))) + (if (not (assq (current-buffer) erc-modified-channels-alist)) + ;; Add buffer, faces and counts + (setq erc-modified-channels-alist + (cons (cons (current-buffer) + (cons 1 (erc-track-find-face faces))) + erc-modified-channels-alist)) + ;; Else modify the face for the buffer, if necessary. + (when faces + (let* ((cell (assq (current-buffer) + erc-modified-channels-alist)) + (old-face (cddr cell)) + (new-face (erc-track-find-face + (if old-face + (cons old-face faces) + faces)))) + (setcdr cell (cons (1+ (cadr cell)) new-face))))) + ;; And display it + (erc-modified-channels-display))) + ;; Else if the active buffer is the current buffer, remove it + ;; from our list. + (when (or (erc-buffer-visible (current-buffer)) + (and this-channel + (assq (current-buffer) erc-modified-channels-alist) + (member this-channel erc-track-exclude))) + ;; Remove it from mode-line if buffer is visible or + ;; channel was added to erc-track-exclude recently. + (erc-modified-channels-remove-buffer (current-buffer)) + (erc-modified-channels-display))))) + +(defun erc-faces-in (str) + "Return a list of all faces used in STR." + (let ((i 0) + (m (length str)) + (faces (erc-list (get-text-property 0 'face str)))) + (while (and (setq i (next-single-property-change i 'face str m)) + (not (= i m))) + (dolist (face (erc-list (get-text-property i 'face str))) + (add-to-list 'faces face))) + faces)) + +(erc-assert + (let ((str "is bold")) + (put-text-property 3 (length str) + 'face '(bold erc-current-nick-face) + str) + (erc-faces-in str))) + +(defun erc-find-parsed-property () + "Find the next occurrence of the `erc-parsed' text property." + (text-property-not-all (point-min) (point-max) 'erc-parsed nil)) + +;;; Buffer switching + +(defvar erc-track-last-non-erc-buffer nil + "Stores the name of the last buffer you were in before activating +`erc-track-switch-buffers'") + +(defun erc-track-sort-by-activest () + "Sort erc-modified-channels-alist by activity. +That means the number of unseen messages in a channel." + (setq erc-modified-channels-alist + (sort erc-modified-channels-alist + (lambda (a b) (> (nth 1 a) (nth 1 b)))))) + +(defun erc-track-get-active-buffer (arg) + "Return the buffer name of ARG in `erc-modified-channels-alist'. +Negative arguments index in the opposite direction. This direction is +relative to `erc-track-switch-direction'" + (let ((dir erc-track-switch-direction) + offset) + (when (< arg 0) + (setq dir (case dir + (oldest 'newest) + (newest 'oldest) + (mostactive 'leastactive) + (leastactive 'mostactive))) + (setq arg (- arg))) + (setq offset (case dir + ((oldest leastactive) + (- (length erc-modified-channels-alist) arg)) + (t (1- arg)))) + ;; normalise out of range user input + (cond ((>= offset (length erc-modified-channels-alist)) + (setq offset (1- (length erc-modified-channels-alist)))) + ((< offset 0) + (setq offset 0))) + (car (nth offset erc-modified-channels-alist)))) + +(defun erc-track-switch-buffer (arg) + "Switch to the next active ERC buffer, or if there are no active buffers, +switch back to the last non-ERC buffer visited. Next is defined by +`erc-track-switch-direction', a negative argument will reverse this." + (interactive "p") + (when erc-track-mode + (cond (erc-modified-channels-alist + ;; if we're not in erc-mode, set this buffer to return to + (unless (eq major-mode 'erc-mode) + (setq erc-track-last-non-erc-buffer (current-buffer))) + ;; and jump to the next active channel + (switch-to-buffer (erc-track-get-active-buffer arg))) + ;; if no active channels, switch back to what we were doing before + ((and erc-track-last-non-erc-buffer + erc-track-switch-from-erc + (buffer-live-p erc-track-last-non-erc-buffer)) + (switch-to-buffer erc-track-last-non-erc-buffer))))) + +;; These bindings are global, because they pop us from any other +;; buffer to an active ERC buffer! + +(global-set-key (kbd "C-c C-@") 'erc-track-switch-buffer) +(global-set-key (kbd "C-c C-SPC") 'erc-track-switch-buffer) + +(provide 'erc-track) + +;;; erc-track.el ends here +;; +;; Local Variables: +;; indent-tabs-mode: t +;; tab-width: 8 +;; End: + +;; arch-tag: 11b439f5-e5d7-4c6c-bb3f-eda98f9b0ac1