2009-10-14 Reiner Steib <Reiner.Steib@gmx.de>
[gnus] / lisp / gnus-registry.el
index f2f7b6f..901e09d 100644 (file)
@@ -1,27 +1,25 @@
 ;;; gnus-registry.el --- article registry for Gnus
 
-;; Copyright (C) 1996, 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004,
-;;   2005, 2006, 2007, 2008 Free Software Foundation, Inc.
+;;; Copyright (C) 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009 
+;;; Free Software Foundation, Inc.
 
 ;; Author: Ted Zlatanov <tzz@lifelogs.com>
-;; Keywords: news
+;; Keywords: news registry
 
 ;; This file is part of GNU Emacs.
 
-;; GNU Emacs is free software; you can redistribute it and/or modify
+;; GNU Emacs is free software: you can redistribute it and/or modify
 ;; it under the terms of the GNU General Public License as published by
-;; the Free Software Foundation; either version 3, or (at your option)
-;; any later version.
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
 
 ;; GNU Emacs is distributed in the hope that it will be useful,
 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
-;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.         See the
+;; 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.
+;; along with GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.
 
 ;;; Commentary:
 
   "*The article registry by Message ID.")
 
 (defcustom gnus-registry-marks
-  '(Important Work Personal To-Do Later)
-  "List of marks that `gnus-registry-mark-article' will offer for completion."
+  '((Important
+     :char ?i
+     :image "summary_important")
+    (Work
+     :char ?w
+     :image "summary_work")
+    (Personal
+     :char ?p
+     :image "summary_personal")
+    (To-Do
+     :char ?t
+     :image "summary_todo")
+    (Later
+     :char ?l
+     :image "summary_later"))
+
+  "List of registry marks and their options.
+
+`gnus-registry-mark-article' will offer symbols from this list
+for completion.  
+
+Each entry must have a character to be useful for summary mode
+line display and for keyboard shortcuts.
+
+Each entry must have an image string to be useful for visual
+display."
   :group 'gnus-registry
-  :type '(repeat symbol))
+  :type '(repeat :tag "Registry Marks"
+                (cons :tag "Mark"
+                      (symbol :tag "Name")
+                      (checklist :tag "Options" :greedy t
+                                 (group :inline t
+                                        (const :format "" :value :char)
+                                        (character :tag "Character code"))
+                                 (group :inline t
+                                        (const :format "" :value :image)
+                                        (string :tag "Image"))))))
 
 (defcustom gnus-registry-default-mark 'To-Do
-  "The default mark."
+  "The default mark.  Should be a valid key for `gnus-registry-marks'."
   :group 'gnus-registry
   :type 'symbol)
 
-(defcustom gnus-registry-unfollowed-groups '("delayed$" "drafts$" "queue$" "INBOX$")
+(defcustom gnus-registry-unfollowed-groups 
+  '("delayed$" "drafts$" "queue$" "INBOX$")
   "List of groups that gnus-registry-split-fancy-with-parent won't return.
 The group names are matched, they don't have to be fully
 qualified.  This parameter tells the Registry 'never split a
@@ -98,10 +130,12 @@ references.'"
   :group 'gnus-registry
   :type '(repeat regexp))
 
-(defcustom gnus-registry-install nil
+(defcustom gnus-registry-install 'ask
   "Whether the registry should be installed."
   :group 'gnus-registry
-  :type 'boolean)
+  :type '(choice (const :tag "Never Install" nil)
+                (const :tag "Always Install" t)
+                (const :tag "Ask Me" ask)))
 
 (defcustom gnus-registry-clean-empty t
   "Whether the empty registry entries should be deleted.
@@ -110,11 +144,17 @@ and no extra data."
   :group 'gnus-registry
   :type 'boolean)
 
-(defcustom gnus-registry-use-long-group-names nil
-  "Whether the registry should use long group names (BUGGY)."
+(defcustom gnus-registry-use-long-group-names t
+  "Whether the registry should use long group names."
   :group 'gnus-registry
   :type 'boolean)
 
+(defcustom gnus-registry-max-track-groups 20
+  "The maximum number of non-unique group matches to check for a message ID."
+  :group 'gnus-registry
+  :type '(radio (const :format "Unlimited " nil)
+               (integer :format "Maximum non-unique matches: %v")))
+
 (defcustom gnus-registry-track-extra nil
   "Whether the registry should track extra data about a message.
 The Subject and Sender (From:) headers are currently tracked this
@@ -125,6 +165,17 @@ way."
     (const :tag "Track by subject (Subject: header)" subject)
     (const :tag "Track by sender (From: header)"  sender)))
 
+(defcustom gnus-registry-split-strategy nil
+  "Whether the registry should track extra data about a message.
+The Subject and Sender (From:) headers are currently tracked this
+way."
+  :group 'gnus-registry
+  :type
+  '(choice :tag "Tracking choices"
+          (const :tag "Only use single choices, discard multiple matches" nil)
+          (const :tag "Majority of matches wins" majority)
+          (const :tag "First found wins"  first)))
+
 (defcustom gnus-registry-entry-caching t
   "Whether the registry should cache extra information."
   :group 'gnus-registry
@@ -197,7 +248,8 @@ considered precious) will not be trimmed."
     (if gnus-save-startup-file-via-temp-buffer
        (let ((coding-system-for-write gnus-ding-file-coding-system)
              (standard-output (current-buffer)))
-         (gnus-gnus-to-quick-newsrc-format t "gnus registry startup file" 'gnus-registry-alist)
+         (gnus-gnus-to-quick-newsrc-format 
+          t "gnus registry startup file" 'gnus-registry-alist)
          (gnus-registry-cache-whitespace file)
          (save-buffer))
       (let ((coding-system-for-write gnus-ding-file-coding-system)
@@ -212,16 +264,15 @@ considered precious) will not be trimmed."
                             (if (and (eq system-type 'ms-dos)
                                      (not (gnus-long-file-names)))
                                 "%s#%d.tm#" ; MSDOS limits files to 8+3
-                              (if (memq system-type '(vax-vms axp-vms))
-                                  "%s$tmp$%d"
-                                "%s#tmp#%d"))
+                              "%s#tmp#%d")
                             working-dir (setq i (1+ i))))
                      (file-exists-p working-file)))
 
        (unwind-protect
            (progn
              (gnus-with-output-to-file working-file
-               (gnus-gnus-to-quick-newsrc-format t "gnus registry startup file" 'gnus-registry-alist))
+               (gnus-gnus-to-quick-newsrc-format 
+                t "gnus registry startup file" 'gnus-registry-alist))
 
              ;; These bindings will mislead the current buffer
              ;; into thinking that it is visiting the startup
@@ -382,7 +433,8 @@ Any entries with extra data (marks, currently) are left alone."
         (subject (gnus-string-remove-all-properties
                   (gnus-registry-simplify-subject
                    (mail-header-subject data-header))))
-        (sender (gnus-string-remove-all-properties (mail-header-from data-header)))
+        (sender (gnus-string-remove-all-properties 
+                 (mail-header-from data-header)))
         (from (gnus-group-guess-full-name-from-command-method from))
         (to (if to (gnus-group-guess-full-name-from-command-method to) nil))
         (to-name (if to to "the Bit Bucket"))
@@ -447,7 +499,7 @@ See the Info node `(gnus)Fancy Mail Splitting' for more details."
              nnmail-split-fancy-with-parent-ignore-groups
            (list nnmail-split-fancy-with-parent-ignore-groups)))
         (log-agent "gnus-registry-split-fancy-with-parent")
-        found)
+        found found-full)
 
     ;; this is a big if-else statement.  it uses
     ;; gnus-registry-post-process-groups to filter the results after
@@ -460,7 +512,9 @@ See the Info node `(gnus)Fancy Mail Splitting' for more details."
         9
         "%s is looking for matches for reference %s from [%s]"
         log-agent reference refstr)
-       (dolist (group (gnus-registry-fetch-groups reference))
+       (dolist (group (gnus-registry-fetch-groups 
+                       reference 
+                       gnus-registry-max-track-groups))
          (when (and group (gnus-registry-follow-group-p group))
            (gnus-message
             7
@@ -468,11 +522,15 @@ See the Info node `(gnus)Fancy Mail Splitting' for more details."
             log-agent reference refstr group)
            (push group found))))
       ;; filter the found groups and return them
-      (setq found (gnus-registry-post-process-groups "references" refstr found)))
-
+      ;; the found groups are the full groups
+      (setq found (gnus-registry-post-process-groups 
+                  "references" refstr found found)))
+     
      ;; else: there were no matches, now try the extra tracking by sender
-     ((and (gnus-registry-track-sender-p) 
-          sender)
+     ((and (gnus-registry-track-sender-p)
+          sender
+          (not (equal (gnus-extract-address-component-email sender)
+                      user-mail-address)))
       (maphash
        (lambda (key value)
         (let ((this-sender (cdr
@@ -480,7 +538,12 @@ See the Info node `(gnus)Fancy Mail Splitting' for more details."
               matches)
           (when (and this-sender
                      (equal sender this-sender))
-            (setq found (append (gnus-registry-fetch-groups key) found))
+            (let ((groups (gnus-registry-fetch-groups 
+                           key
+                           gnus-registry-max-track-groups)))
+              (dolist (group groups)
+                (push group found-full)
+                (setq found (append (list group) (delete group found)))))
             (push key matches)
             (gnus-message
              ;; raise level of messaging if gnus-registry-track-extra
@@ -489,7 +552,9 @@ See the Info node `(gnus)Fancy Mail Splitting' for more details."
              log-agent sender found matches))))
        gnus-registry-hashtb)
       ;; filter the found groups and return them
-      (setq found (gnus-registry-post-process-groups "sender" sender found)))
+      ;; the found groups are NOT the full groups
+      (setq found (gnus-registry-post-process-groups 
+                  "sender" sender found found-full)))
       
      ;; else: there were no matches, now try the extra tracking by subject
      ((and (gnus-registry-track-subject-p)
@@ -502,7 +567,12 @@ See the Info node `(gnus)Fancy Mail Splitting' for more details."
               matches)
           (when (and this-subject
                      (equal subject this-subject))
-            (setq found (append (gnus-registry-fetch-groups key) found))
+            (let ((groups (gnus-registry-fetch-groups 
+                           key
+                           gnus-registry-max-track-groups)))
+              (dolist (group groups)
+                (push group found-full)
+                (setq found (append (list group) (delete group found)))))
             (push key matches)
             (gnus-message
              ;; raise level of messaging if gnus-registry-track-extra
@@ -511,10 +581,14 @@ See the Info node `(gnus)Fancy Mail Splitting' for more details."
              log-agent subject found matches))))
        gnus-registry-hashtb)
       ;; filter the found groups and return them
-      (setq found (gnus-registry-post-process-groups "subject" subject found))))))
+      ;; the found groups are NOT the full groups
+      (setq found (gnus-registry-post-process-groups 
+                  "subject" subject found found-full))))
+    ;; after the (cond) we extract the actual value safely
+    (car-safe found)))
 
-(defun gnus-registry-post-process-groups (mode key groups)
-  "Modifies GROUPS obtained by searching by MODE for KEY to determine which ones to follow.
+(defun gnus-registry-post-process-groups (mode key groups groups-full)
+  "Modifies GROUPS found by MODE for KEY to determine which ones to follow.
 
 MODE can be 'subject' or 'sender' for example.  The KEY is the
 value by which MODE was searched.
@@ -527,9 +601,28 @@ This is not possible if gnus-registry-use-long-group-names is
 false.  Foreign methods are not supported so they are rejected.
 
 Reduces the list to a single group, or complains if that's not
-possible."
+possible.  Uses `gnus-registry-split-strategy' and GROUPS-FULL if
+necessary."
   (let ((log-agent "gnus-registry-post-process-group")
        out)
+
+    ;; the strategy can be 'first, 'majority, or nil
+    (when (eq gnus-registry-split-strategy 'first)
+      (when groups
+       (setq groups (list (car-safe groups)))))
+
+    (when (eq gnus-registry-split-strategy 'majority)
+      (let ((freq (make-hash-table
+                  :size 256
+                  :test 'equal)))
+       (mapc (lambda(x) (puthash x (1+ (gethash x freq 0)) freq)) groups-full)
+       (setq groups (list (car-safe
+                           (sort
+                            groups
+                            (lambda (a b)
+                              (> (gethash a freq 0)
+                                 (gethash b freq 0)))))))))
+    
     (if gnus-registry-use-long-group-names
        (dolist (group groups)
          (let ((m1 (gnus-find-method-for-group group))
@@ -596,9 +689,8 @@ Consults `gnus-registry-unfollowed-groups' and
   (let (articles)
     (maphash
      (lambda (key value)
-       (when (gnus-registry-grep-in-list
-             keyword
-             (cdr (gnus-registry-fetch-extra key 'keywords)))
+       (when (member keyword
+                  (cdr-safe (gnus-registry-fetch-extra key 'keywords)))
         (push key articles)))
      gnus-registry-hashtb)
     articles))
@@ -648,16 +740,109 @@ Consults `gnus-registry-unfollowed-groups' and
                          (assoc article (gnus-data-list nil)))))
     nil))
 
-;;; this should be redone with catch/throw
 (defun gnus-registry-grep-in-list (word list)
-  (when word
-    (memq nil
-         (mapcar 'not
-                 (mapcar
-                  (lambda (x)
-                    (string-match word x))
-                  list)))))
-
+"Find if a WORD matches any regular expression in the given LIST."
+  (when (and word list)
+    (catch 'found
+      (dolist (r list)
+       (when (string-match r word)
+         (throw 'found r))))))
+
+(defun gnus-registry-do-marks (type function)
+  "For each known mark, call FUNCTION for each cell of type TYPE.
+
+FUNCTION should take two parameters, a mark symbol and the cell value."
+  (dolist (mark-info gnus-registry-marks)
+    (let* ((mark (car-safe mark-info))
+          (data (cdr-safe mark-info))
+          (cell-data (plist-get data type)))
+      (when cell-data
+       (funcall function mark cell-data)))))
+
+;;; this is ugly code, but I don't know how to do it better
+(defun gnus-registry-install-shortcuts ()
+  "Install the keyboard shortcuts and menus for the registry.
+Uses `gnus-registry-marks' to find what shortcuts to install."
+  (let (keys-plist)
+    (gnus-registry-do-marks 
+     :char
+     (lambda (mark data)
+       (let ((function-format
+             (format "gnus-registry-%%s-article-%s-mark" mark)))
+
+;;; The following generates these functions:
+;;; (defun gnus-registry-set-article-Important-mark (&rest articles)
+;;;   "Apply the Important mark to process-marked ARTICLES."
+;;;   (interactive (gnus-summary-work-articles current-prefix-arg))
+;;;   (gnus-registry-set-article-mark-internal 'Important articles nil t))
+;;; (defun gnus-registry-remove-article-Important-mark (&rest articles)
+;;;   "Apply the Important mark to process-marked ARTICLES."
+;;;   (interactive (gnus-summary-work-articles current-prefix-arg))
+;;;   (gnus-registry-set-article-mark-internal 'Important articles t t))
+
+        (dolist (remove '(t nil))
+          (let* ((variant-name (if remove "remove" "set"))
+                 (function-name (format function-format variant-name))
+                 (shortcut (format "%c" data))
+                 (shortcut (if remove (upcase shortcut) shortcut)))
+            (unintern function-name)
+            (eval
+             `(defun 
+                ;; function name
+                ,(intern function-name) 
+                ;; parameter definition
+                (&rest articles)
+                ;; documentation
+                ,(format 
+                  "%s the %s mark over process-marked ARTICLES."
+                  (upcase-initials variant-name)
+                  mark)
+                ;; interactive definition
+                (interactive 
+                 (gnus-summary-work-articles current-prefix-arg))
+                ;; actual code
+
+                ;; if this is called and the user doesn't want the
+                ;; registry enabled, we'll ask anyhow
+                (when (eq gnus-registry-install nil)
+                  (setq gnus-registry-install 'ask))
+
+                ;; now the user is asked if gnus-registry-install is 'ask
+                (when (gnus-registry-install-p)
+                  (gnus-registry-set-article-mark-internal 
+                   ;; all this just to get the mark, I must be doing it wrong
+                   (intern ,(symbol-name mark))
+                   articles ,remove t)
+                  (dolist (article articles)
+                    (gnus-summary-update-article 
+                     article 
+                     (assoc article (gnus-data-list nil)))))))
+            (push (intern function-name) keys-plist)
+            (push shortcut keys-plist)
+            (gnus-message 
+             9 
+             "Defined mark handling function %s" 
+             function-name))))))
+    (gnus-define-keys-1
+     '(gnus-registry-mark-map "M" gnus-summary-mark-map) 
+     keys-plist)))
+
+;;; use like this:
+;;; (defalias 'gnus-user-format-function-M 
+;;;           'gnus-registry-user-format-function-M)
+(defun gnus-registry-user-format-function-M (headers)
+  (let* ((id (mail-header-message-id headers))
+        (marks (when id (gnus-registry-fetch-extra-marks id))))
+    (apply 'concat (mapcar (lambda(mark)
+                            (let ((c 
+                                   (plist-get
+                                    (cdr-safe 
+                                     (assoc mark gnus-registry-marks))
+                                    :char)))
+                              (if c
+                                  (list c)
+                                nil)))
+                          marks))))
 
 (defun gnus-registry-read-mark ()
   "Read a mark name from the user with completion."
@@ -665,7 +850,7 @@ Consults `gnus-registry-unfollowed-groups' and
               (symbol-name gnus-registry-default-mark)
               "Label" 
               (mapcar (lambda (x)      ; completion list
-                        (cons (symbol-name x) x))
+                        (cons (symbol-name (car-safe x)) (car-safe x)))
                       gnus-registry-marks))))
     (when (stringp mark)
       (intern mark))))
@@ -829,8 +1014,8 @@ Returns the first place where the trail finds a group name."
                       crumb
                     (gnus-group-short-name crumb))))))))
 
-(defun gnus-registry-fetch-groups (id)
-  "Get the groups of a message, based on the message ID."
+(defun gnus-registry-fetch-groups (id &optional max)
+  "Get the groups (up to MAX, if given) of a message, based on the message ID."
   (let ((trail (gethash id gnus-registry-hashtb))
        groups)
     (dolist (crumb trail)
@@ -842,7 +1027,9 @@ Returns the first place where the trail finds a group name."
          (if (or (not (stringp crumb)) gnus-registry-use-long-group-names)
              crumb
            (gnus-group-short-name crumb))
-        groups))))
+        groups))
+       (when (and max (> (length groups) max))
+         (return))))
     ;; return the list of groups
     groups))
 
@@ -924,9 +1111,12 @@ Returns the first place where the trail finds a group name."
 
 ;;;###autoload
 (defun gnus-registry-initialize ()
+"Initialize the Gnus registry."
   (interactive)
-  (setq gnus-registry-install t)
+  (gnus-message 5 "Initializing the registry")
+  (setq gnus-registry-install t)       ; in case it was 'ask or nil
   (gnus-registry-install-hooks)
+  (gnus-registry-install-shortcuts)
   (gnus-registry-read))
 
 ;;;###autoload
@@ -958,13 +1148,27 @@ Returns the first place where the trail finds a group name."
 
 (add-hook 'gnus-registry-unload-hook 'gnus-registry-unload-hook)
 
-(when gnus-registry-install
-  (gnus-registry-install-hooks)
-  (gnus-registry-read))
-
-;; TODO: a lot of things
+(defun gnus-registry-install-p ()
+  (interactive)
+  (when (eq gnus-registry-install 'ask)
+    (setq gnus-registry-install
+         (gnus-y-or-n-p
+          (concat "Enable the Gnus registry?  "
+                  "See the variable `gnus-registry-install' "
+                  "to get rid of this query permanently. ")))
+    (when gnus-registry-install
+      ;; we just set gnus-registry-install to t, so initialize the registry!
+      (gnus-registry-initialize)))
+;;; we could call it here: (customize-variable 'gnus-registry-install)
+  gnus-registry-install)
+
+(when (or (eq gnus-registry-install t)
+         (gnus-registry-install-p))
+  (gnus-registry-initialize))
+
+;; TODO: a few things
 
 (provide 'gnus-registry)
 
-;;; arch-tag: 5cba0a32-718a-4a97-8c91-0a15af21da94
+;; arch-tag: 5cba0a32-718a-4a97-8c91-0a15af21da94
 ;;; gnus-registry.el ends here