*** empty log message ***
[gnus] / lisp / gnus-topic.el
1 ;;; gnus-topic.el --- a folding minor mode for Gnus group buffers
2 ;; Copyright (C) 1995,96 Free Software Foundation, Inc.
3
4 ;; Author: Ilja Weis <kult@uni-paderborn.de>
5 ;;      Lars Magne Ingebrigtsen <larsi@ifi.uio.no>
6 ;; Keywords: news
7
8 ;; This file is part of GNU Emacs.
9
10 ;; GNU Emacs is free software; you can redistribute it and/or modify
11 ;; it under the terms of the GNU General Public License as published by
12 ;; the Free Software Foundation; either version 2, or (at your option)
13 ;; any later version.
14
15 ;; GNU Emacs is distributed in the hope that it will be useful,
16 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
17 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18 ;; GNU General Public License for more details.
19
20 ;; You should have received a copy of the GNU General Public License
21 ;; along with GNU Emacs; see the file COPYING.  If not, write to the
22 ;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
23 ;; Boston, MA 02111-1307, USA.
24
25 ;;; Commentary:
26
27 ;;; Code:
28
29 (require 'gnus)
30 (eval-when-compile (require 'cl))
31
32 (defvar gnus-topic-mode nil
33   "Minor mode for Gnus group buffers.")
34
35 (defvar gnus-topic-mode-hook nil
36   "Hook run in topic mode buffers.")
37
38 (defvar gnus-topic-line-format "%i[ %(%{%n%}%) -- %A ]%v\n"
39   "Format of topic lines.
40 It works along the same lines as a normal formatting string,
41 with some simple extensions.
42
43 %i  Indentation based on topic level.
44 %n  Topic name.
45 %v  Nothing if the topic is visible, \"...\" otherwise.
46 %g  Number of groups in the topic.
47 %a  Number of unread articles in the groups in the topic.
48 %A  Number of unread articles in the groups in the topic and its subtopics.
49 ")
50
51 (defvar gnus-topic-unique t
52   "*If non-nil, each group will only belong to one topic.")
53
54 (defvar gnus-topic-indent-level 2
55   "*How much each subtopic should be indented.")
56
57 ;; Internal variables.
58
59 (defvar gnus-topic-active-topology nil)
60 (defvar gnus-topic-active-alist nil)
61
62 (defvar gnus-topic-killed-topics nil)
63 (defvar gnus-topic-inhibit-change-level nil)
64 (defvar gnus-topic-tallied-groups nil)
65
66 (defconst gnus-topic-line-format-alist
67   `((?n name ?s)
68     (?v visible ?s)
69     (?i indentation ?s)
70     (?g number-of-groups ?d)
71     (?a (gnus-topic-articles-in-topic entries) ?d)
72     (?A total-number-of-articles ?d)
73     (?l level ?d)))
74
75 (defvar gnus-topic-line-format-spec nil)
76
77 ;; Functions.
78
79 (defun gnus-group-topic-name ()
80   "The name of the topic on the current line."
81   (let ((topic (get-text-property (gnus-point-at-bol) 'gnus-topic)))
82     (and topic (symbol-name topic))))
83
84 (defun gnus-group-topic-level ()
85   "The level of the topic on the current line."
86   (get-text-property (gnus-point-at-bol) 'gnus-topic-level))
87
88 (defun gnus-group-topic-unread ()
89   "The number of unread articles in topic on the current line."
90   (get-text-property (gnus-point-at-bol) 'gnus-topic-unread))
91
92 (defun gnus-topic-init-alist ()
93   "Initialize the topic structures."
94   (setq gnus-topic-topology
95         (cons (list "Gnus" 'visible)
96               (mapcar (lambda (topic)
97                         (list (list (car topic) 'visible)))
98                       '(("misc")))))
99   (setq gnus-topic-alist
100         (list (cons "misc"
101                     (mapcar (lambda (info) (gnus-info-group info))
102                             (cdr gnus-newsrc-alist)))
103               (list "Gnus")))
104   (gnus-topic-enter-dribble))
105
106 (defun gnus-group-prepare-topics (level &optional all lowest regexp list-topic topic-level)
107   "List all newsgroups with unread articles of level LEVEL or lower, and
108 use the `gnus-group-topics' to sort the groups.
109 If ALL is non-nil, list groups that have no unread articles.
110 If LOWEST is non-nil, list all newsgroups of level LOWEST or higher."
111   (set-buffer gnus-group-buffer)
112   (let ((buffer-read-only nil)
113         (lowest (or lowest 1)))
114
115     (setq gnus-topic-tallied-groups nil)
116
117     (unless gnus-topic-alist
118       (gnus-topic-check-topology))
119
120     (unless list-topic 
121       (erase-buffer))
122     
123     ;; List dead groups?
124     (when (and (>= level gnus-level-zombie) (<= lowest gnus-level-zombie))
125       (gnus-group-prepare-flat-list-dead 
126        (setq gnus-zombie-list (sort gnus-zombie-list 'string<)) 
127        gnus-level-zombie ?Z
128        regexp))
129     
130     (when (and (>= level gnus-level-killed) (<= lowest gnus-level-killed))
131       (gnus-group-prepare-flat-list-dead 
132        (setq gnus-killed-list (sort gnus-killed-list 'string<))
133        gnus-level-killed ?K
134        regexp))
135
136     ;; Use topics.
137     (when (< lowest gnus-level-zombie)
138       (if list-topic
139           (let ((top (gnus-topic-find-topology list-topic)))
140             (gnus-topic-prepare-topic (cdr top) (car top)
141                                       (or topic-level level) all))
142         (gnus-topic-prepare-topic gnus-topic-topology 0
143                                   (or topic-level level) all))))
144
145   (gnus-group-set-mode-line)
146   (setq gnus-group-list-mode (cons level all))
147   (run-hooks 'gnus-group-prepare-hook))
148
149 (defun gnus-topic-prepare-topic (topicl level &optional list-level all silent)
150   "Insert TOPIC into the group buffer.
151 If SILENT, don't insert anything.  Return the number of unread
152 articles in the topic and its subtopics."
153   (let* ((type (pop topicl))
154          (entries (gnus-topic-find-groups (car type) list-level all))
155          (visiblep (and (eq (nth 1 type) 'visible) (not silent)))
156          (gnus-group-indentation 
157           (make-string (* gnus-topic-indent-level level) ? ))
158          (beg (progn (beginning-of-line) (point)))
159          (topicl (reverse topicl))
160          (all-entries entries)
161          (unread 0)
162          (topic (car type))
163          info entry end active)
164     ;; Insert any sub-topics.
165     (while topicl
166       (incf unread
167             (gnus-topic-prepare-topic 
168              (pop topicl) (1+ level) list-level all
169              (not visiblep))))
170     (setq end (point))
171     (goto-char beg)
172     ;; Insert all the groups that belong in this topic.
173     (while (setq entry (pop entries))
174       (when visiblep 
175         (if (stringp entry)
176             ;; Dead groups.
177             (gnus-group-insert-group-line
178              entry (if (member entry gnus-zombie-list) 8 9)
179              nil (- (1+ (cdr (setq active (gnus-active entry))))
180                     (car active)) nil)
181           ;; Living groups.
182           (when (setq info (nth 2 entry))
183             (gnus-group-insert-group-line 
184              (gnus-info-group info)
185              (gnus-info-level info) (gnus-info-marks info) 
186              (car entry) (gnus-info-method info)))))
187       (when (and (listp entry)
188                  (numberp (car entry))
189                  (not (member (gnus-info-group (setq info (nth 2 entry)))
190                               gnus-topic-tallied-groups)))
191         (push (gnus-info-group info) gnus-topic-tallied-groups)
192         (incf unread (car entry))))
193     (goto-char beg)
194     ;; Insert the topic line.
195     (unless silent
196       (gnus-extent-start-open (point))
197       (gnus-topic-insert-topic-line 
198        (car type) visiblep
199        (not (eq (nth 2 type) 'hidden))
200        level all-entries unread))
201     (goto-char end)
202     unread))
203
204 (defun gnus-topic-find-groups (topic &optional level all)
205   "Return entries for all visible groups in TOPIC."
206   (let ((groups (cdr (assoc topic gnus-topic-alist)))
207         info clevel unread group lowest params visible-groups entry active)
208     (setq lowest (or lowest 1))
209     (setq level (or level 7))
210     ;; We go through the newsrc to look for matches.
211     (while groups
212       (setq entry (gnus-gethash (setq group (pop groups)) gnus-newsrc-hashtb)
213             info (nth 2 entry)
214             params (gnus-info-params info)
215             active (gnus-active group)
216             unread (or (car entry)
217                        (and (not (equal group "dummy.group"))
218                             active
219                             (- (1+ (cdr active)) (car active))))
220             clevel (or (gnus-info-level info)
221                        (if (member group gnus-zombie-list) 8 9)))
222       (and 
223        unread                           ; nil means that the group is dead.
224        (<= clevel level) 
225        (>= clevel lowest)               ; Is inside the level we want.
226        (or all
227            (if (eq unread t)
228                gnus-group-list-inactive-groups
229              (> unread 0))
230            (and gnus-list-groups-with-ticked-articles
231                 (cdr (assq 'tick (gnus-info-marks info))))
232                                         ; Has right readedness.
233            ;; Check for permanent visibility.
234            (and gnus-permanently-visible-groups
235                 (string-match gnus-permanently-visible-groups group))
236            (memq 'visible params)
237            (cdr (assq 'visible params)))
238        ;; Add this group to the list of visible groups.
239        (push (or entry group) visible-groups)))
240     (nreverse visible-groups)))
241
242 (defun gnus-topic-remove-topic (&optional insert total-remove hide in-level)
243   "Remove the current topic."
244   (let ((topic (gnus-group-topic-name))
245         (level (gnus-group-topic-level))
246         (beg (progn (beginning-of-line) (point)))
247         buffer-read-only)
248     (when topic
249       (while (and (zerop (forward-line 1))
250                   (> (or (gnus-group-topic-level) (1+ level)) level)))
251       (delete-region beg (point))
252       (setcar (cdadr (gnus-topic-find-topology topic))
253               (if insert 'visible 'invisible))
254       (when hide
255         (setcdr (cdadr (gnus-topic-find-topology topic))
256                 (list hide)))
257       (unless total-remove
258         (gnus-topic-insert-topic topic in-level)))))
259
260 (defun gnus-topic-insert-topic (topic &optional level)
261   "Insert TOPIC."
262   (gnus-group-prepare-topics 
263    (car gnus-group-list-mode) (cdr gnus-group-list-mode)
264    nil nil topic level))
265   
266 (defun gnus-topic-fold (&optional insert)
267   "Remove/insert the current topic."
268   (let ((topic (gnus-group-topic-name))) 
269     (when topic
270       (save-excursion
271         (if (not (gnus-group-active-topic-p))
272             (gnus-topic-remove-topic
273              (or insert (not (gnus-topic-visible-p))))
274           (let ((gnus-topic-topology gnus-topic-active-topology)
275                 (gnus-topic-alist gnus-topic-active-alist)
276                 (gnus-group-list-mode (cons 5 t)))
277             (gnus-topic-remove-topic
278              (or insert (not (gnus-topic-visible-p))) nil nil 9)))))))
279
280 (defun gnus-group-topic-p ()
281   "Return non-nil if the current line is a topic."
282   (gnus-group-topic-name))
283
284 (defun gnus-topic-visible-p ()
285   "Return non-nil if the current topic is visible."
286   (get-text-property (gnus-point-at-bol) 'gnus-topic-visible))
287
288 (defun gnus-topic-insert-topic-line (name visiblep shownp level entries 
289                                           &optional unread)
290   (let* ((visible (if visiblep "" "..."))
291          (indentation (make-string (* gnus-topic-indent-level level) ? ))
292          (total-number-of-articles unread)
293          (number-of-groups (length entries))
294          (active-topic (eq gnus-topic-alist gnus-topic-active-alist)))
295     (beginning-of-line)
296     ;; Insert the text.
297     (gnus-add-text-properties 
298      (point)
299      (prog1 (1+ (point)) 
300        (eval gnus-topic-line-format-spec)
301        (gnus-topic-remove-excess-properties)1)
302      (list 'gnus-topic (intern name)
303            'gnus-topic-level level
304            'gnus-topic-unread unread
305            'gnus-active active-topic
306            'gnus-topic-visible visiblep))))
307
308 (defun gnus-topic-previous-topic (topic)
309   "Return the previous topic on the same level as TOPIC."
310   (let ((top (cddr (gnus-topic-find-topology
311                     (gnus-topic-parent-topic topic)))))
312     (unless (equal topic (caaar top))
313       (while (and top (not (equal (caaadr top) topic)))
314         (setq top (cdr top)))
315       (caaar top))))
316
317 (defun gnus-topic-parent-topic (topic &optional topology)
318   "Return the parent of TOPIC."
319   (unless topology
320     (setq topology gnus-topic-topology))
321   (let ((parent (car (pop topology)))
322         result found)
323     (while (and topology
324                 (not (setq found (equal (caaar topology) topic)))
325                 (not (setq result (gnus-topic-parent-topic topic 
326                                                            (car topology)))))
327       (setq topology (cdr topology)))
328     (or result (and found parent))))
329
330 (defun gnus-topic-next-topic (topic &optional previous)
331   "Return the next sibling of TOPIC."
332   (let ((topology gnus-topic-topology)
333         (parentt (cddr (gnus-topic-find-topology 
334                         (gnus-topic-parent-topic topic))))
335         prev)
336     (while (and parentt
337                 (not (equal (caaar parentt) topic)))
338       (setq prev (caaar parentt)
339             parentt (cdr parentt)))
340     (if previous
341         prev
342       (caaadr parentt))))
343
344 (defun gnus-topic-find-topology (topic &optional topology level remove)
345   "Return the topology of TOPIC."
346   (unless topology
347     (setq topology gnus-topic-topology)
348     (setq level 0))
349   (let ((top topology)
350         result)
351     (if (equal (caar topology) topic)
352         (progn
353           (when remove
354             (delq topology remove))
355           (cons level topology))
356       (setq topology (cdr topology))
357       (while (and topology
358                   (not (setq result (gnus-topic-find-topology
359                                      topic (car topology) (1+ level)
360                                      (and remove top)))))
361         (setq topology (cdr topology)))
362       result)))
363
364 (gnus-add-shutdown 'gnus-topic-close 'gnus)
365
366 (defun gnus-topic-close ()
367   (setq gnus-topic-active-topology nil
368         gnus-topic-active-alist nil
369         gnus-topic-killed-topics nil
370         gnus-topic-tallied-groups nil))
371
372 (defun gnus-topic-check-topology ()  
373   ;; The first time we set the topology to whatever we have
374   ;; gotten here, which can be rather random.
375   (unless gnus-topic-alist
376     (gnus-topic-init-alist))
377
378   (let ((topics (gnus-topic-list))
379         (alist gnus-topic-alist)
380         changed)
381     (while alist
382       (unless (member (caar alist) topics)
383         (nconc gnus-topic-topology
384                (list (list (list (caar alist) 'visible))))
385         (setq changed t))
386       (setq alist (cdr alist)))
387     (when changed
388       (gnus-topic-enter-dribble)))
389   (let* ((tgroups (apply 'append (mapcar (lambda (entry) (cdr entry))
390                                          gnus-topic-alist)))
391          (entry (assoc (caar gnus-topic-topology) gnus-topic-alist))
392          (newsrc gnus-newsrc-alist)
393          group)
394     (while newsrc
395       (unless (member (setq group (gnus-info-group (pop newsrc))) tgroups)
396         (setcdr entry (cons group (cdr entry)))))))
397
398 (defvar gnus-tmp-topics nil)
399 (defun gnus-topic-list (&optional topology)
400   (unless topology
401     (setq topology gnus-topic-topology 
402           gnus-tmp-topics nil))
403   (push (caar topology) gnus-tmp-topics)
404   (mapcar 'gnus-topic-list (cdr topology))
405   gnus-tmp-topics)
406
407 (defun gnus-topic-enter-dribble ()
408   (gnus-dribble-enter
409    (format "(setq gnus-topic-topology '%S)" gnus-topic-topology)))
410
411 (defun gnus-topic-articles-in-topic (entries)
412   (let ((total 0)
413         number)
414     (while entries
415       (when (numberp (setq number (car (pop entries))))
416         (incf total number)))
417     total))
418
419 (defun gnus-group-parent-topic ()
420   "Return the topic the current group belongs in."
421   (let ((group (gnus-group-group-name)))
422     (if group
423         (gnus-group-topic group)
424       (gnus-group-topic-name))))
425
426 (defun gnus-group-topic (group)
427   "Return the topic GROUP is a member of."
428   (let ((alist gnus-topic-alist)
429         out)
430     (while alist
431       (when (member group (cdar alist))
432         (setq out (caar alist)
433               alist nil))
434       (setq alist (cdr alist)))
435     out))
436
437 (defun gnus-topic-goto-topic (topic)
438   (when topic
439     (gnus-goto-char (text-property-any (point-min) (point-max)
440                                        'gnus-topic (intern topic)))))
441   
442 (defun gnus-topic-update-topic ()
443   "Update all parent topics to the current group."
444   (when (and (eq major-mode 'gnus-group-mode)
445              gnus-topic-mode)
446     (let ((group (gnus-group-group-name))
447           (buffer-read-only nil))
448       (when (and group (gnus-get-info group)
449                  (gnus-topic-goto-topic (gnus-group-parent-topic)))
450         (gnus-topic-update-topic-line (gnus-group-topic-name))
451         (gnus-group-goto-group group)
452         (gnus-group-position-point)))))
453
454 (defun gnus-topic-goto-missing-group (group) 
455   "Place point where GROUP is supposed to be inserted."
456   (let* ((topic (gnus-group-topic group))
457          (groups (cdr (assoc topic gnus-topic-alist)))
458          (g (cdr (member group groups)))
459          (unfound t))
460     (while (and g unfound)
461       (when (gnus-group-goto-group (pop g))
462         (beginning-of-line)
463         (setq unfound nil)))
464     (when unfound
465       (setq g (cdr (member group (reverse groups))))
466       (while (and g unfound)
467         (when (gnus-group-goto-group (pop g))
468           (forward-line 1)
469           (setq unfound nil)))
470       (when unfound
471         (gnus-topic-goto-topic topic)
472         (forward-line 1)))))
473
474 (defun gnus-topic-update-topic-line (topic-name &optional reads)
475   (let* ((type (cadr (gnus-topic-find-topology topic-name)))
476          (entries (gnus-topic-find-groups 
477                    (car type) (car gnus-group-list-mode)
478                    (cdr gnus-group-list-mode)))
479          (parent (gnus-topic-parent-topic topic-name))
480          (all-entries entries)
481          (unread 0)
482          old-unread entry)
483     (when (gnus-topic-goto-topic (car type))
484       ;; Tally all the groups that belong in this topic.
485       (if reads
486           (setq unread (- (gnus-group-topic-unread) reads))
487         (while (setq entry (pop entries))
488           (when (numberp (car entry))
489             (incf unread (car entry)))))
490       (setq old-unread (gnus-group-topic-unread))
491       ;; Insert the topic line.
492       (gnus-topic-insert-topic-line 
493        (car type) (gnus-topic-visible-p)
494        (not (eq (nth 2 type) 'hidden))
495        (gnus-group-topic-level) all-entries unread)
496       (gnus-delete-line))
497     (when parent
498       (forward-line -1)
499       (gnus-topic-update-topic-line
500        parent (- old-unread (gnus-group-topic-unread))))
501     unread))
502
503 (defun gnus-topic-grok-active (&optional force)
504   "Parse all active groups and create topic structures for them."
505   ;; First we make sure that we have really read the active file. 
506   (when (or force
507             (not gnus-topic-active-alist))
508     (let (groups)
509       ;; Get a list of all groups available.
510       (mapatoms (lambda (g) (when (symbol-value g)
511                               (push (symbol-name g) groups)))
512                 gnus-active-hashtb)
513       (setq groups (sort groups 'string<))
514       ;; Init the variables.
515       (setq gnus-topic-active-topology '(("" visible)))
516       (setq gnus-topic-active-alist nil)
517       ;; Descend the top-level hierarchy.
518       (gnus-topic-grok-active-1 gnus-topic-active-topology groups)
519       ;; Set the top-level topic names to something nice.
520       (setcar (car gnus-topic-active-topology) "Gnus active")
521       (setcar (car gnus-topic-active-alist) "Gnus active"))))
522
523 (defun gnus-topic-grok-active-1 (topology groups)
524   (let* ((name (caar topology))
525          (prefix (concat "^" (regexp-quote name)))
526          tgroups ntopology group)
527     (while (and groups
528                 (string-match prefix (setq group (car groups))))
529       (if (not (string-match "\\." group (match-end 0)))
530           ;; There are no further hierarchies here, so we just
531           ;; enter this group into the list belonging to this
532           ;; topic.
533           (push (pop groups) tgroups)
534         ;; New sub-hierarchy, so we add it to the topology.
535         (nconc topology (list (setq ntopology 
536                                     (list (list (substring 
537                                                  group 0 (match-end 0))
538                                                 'invisible)))))
539         ;; Descend the hierarchy.
540         (setq groups (gnus-topic-grok-active-1 ntopology groups))))
541     ;; We remove the trailing "." from the topic name.
542     (setq name
543           (if (string-match "\\.$" name)
544               (substring name 0 (match-beginning 0))
545             name))
546     ;; Add this topic and its groups to the topic alist.
547     (push (cons name (nreverse tgroups)) gnus-topic-active-alist)
548     (setcar (car topology) name)
549     ;; We return the rest of the groups that didn't belong
550     ;; to this topic.
551     groups))
552
553 (defun gnus-group-active-topic-p ()
554   "Return whether the current active comes from the active topics."
555   (save-excursion
556     (beginning-of-line)
557     (get-text-property (point) 'gnus-active)))
558
559 ;;; Topic mode, commands and keymap.
560
561 (defvar gnus-topic-mode-map nil)
562 (defvar gnus-group-topic-map nil)
563
564 (unless gnus-topic-mode-map
565   (setq gnus-topic-mode-map (make-sparse-keymap))
566
567   ;; Override certain group mode keys.
568   (gnus-define-keys
569    gnus-topic-mode-map
570    "=" gnus-topic-select-group
571    "\r" gnus-topic-select-group
572    " " gnus-topic-read-group
573    "\C-k" gnus-topic-kill-group
574    "\C-y" gnus-topic-yank-group
575    "\M-g" gnus-topic-get-new-news-this-topic
576    "AT" gnus-topic-list-active
577    gnus-mouse-2 gnus-mouse-pick-topic)
578
579   ;; Define a new submap.
580   (gnus-define-keys
581    (gnus-group-topic-map "T" gnus-group-mode-map)
582    "#" gnus-topic-mark-topic
583    "\M-#" gnus-topic-unmark-topic
584    "n" gnus-topic-create-topic
585    "m" gnus-topic-move-group
586    "D" gnus-topic-remove-group
587    "c" gnus-topic-copy-group
588    "h" gnus-topic-hide-topic
589    "s" gnus-topic-show-topic
590    "M" gnus-topic-move-matching
591    "C" gnus-topic-copy-matching
592    "\C-i" gnus-topic-indent
593    [tab] gnus-topic-indent
594    "r" gnus-topic-rename
595    "\177" gnus-topic-delete))
596
597 (defun gnus-topic-make-menu-bar ()
598   (unless (boundp 'gnus-topic-menu)
599     (easy-menu-define
600      gnus-topic-menu gnus-topic-mode-map ""
601      '("Topics"
602        ["Toggle topics" gnus-topic-mode t]
603        ("Groups"
604         ["Copy" gnus-topic-copy-group t]
605         ["Move" gnus-topic-move-group t]
606         ["Remove" gnus-topic-remove-group t]
607         ["Copy matching" gnus-topic-copy-matching t]
608         ["Move matching" gnus-topic-move-matching t])
609        ("Topics"
610         ["Show" gnus-topic-show-topic t]
611         ["Hide" gnus-topic-hide-topic t]
612         ["Delete" gnus-topic-delete t]
613         ["Rename" gnus-topic-rename t]
614         ["Create" gnus-topic-create-topic t]
615         ["Mark" gnus-topic-mark-topic t]
616         ["Indent" gnus-topic-indent t])
617        ["List active" gnus-topic-list-active t]))))
618
619 (defun gnus-topic-mode (&optional arg redisplay)
620   "Minor mode for topicsifying Gnus group buffers."
621   (interactive (list current-prefix-arg t))
622   (when (eq major-mode 'gnus-group-mode)
623     (make-local-variable 'gnus-topic-mode)
624     (setq gnus-topic-mode 
625           (if (null arg) (not gnus-topic-mode)
626             (> (prefix-numeric-value arg) 0)))
627     ;; Infest Gnus with topics.
628     (when gnus-topic-mode
629       (when (and menu-bar-mode
630                  (gnus-visual-p 'topic-menu 'menu))
631         (gnus-topic-make-menu-bar))
632       (setq gnus-topic-line-format-spec 
633             (gnus-parse-format gnus-topic-line-format 
634                                gnus-topic-line-format-alist t))
635       (unless (assq 'gnus-topic-mode minor-mode-alist)
636         (push '(gnus-topic-mode " Topic") minor-mode-alist))
637       (unless (assq 'gnus-topic-mode minor-mode-map-alist)
638         (push (cons 'gnus-topic-mode gnus-topic-mode-map)
639               minor-mode-map-alist))
640       (add-hook 'gnus-summary-exit-hook 'gnus-topic-update-topic)
641       (add-hook 'gnus-group-catchup-group-hook 'gnus-topic-update-topic)
642       (make-local-variable 'gnus-group-prepare-function)
643       (setq gnus-group-prepare-function 'gnus-group-prepare-topics)
644       (make-local-variable 'gnus-group-goto-next-group-function)
645       (setq gnus-group-goto-next-group-function 
646             'gnus-topic-goto-next-group)
647       (setq gnus-group-change-level-function 'gnus-topic-change-level)
648       (setq gnus-goto-missing-group-function 'gnus-topic-goto-missing-group)
649       (make-local-variable 'gnus-group-indentation-function)
650       (setq gnus-group-indentation-function
651             'gnus-topic-group-indentation)
652       ;; We check the topology.
653       (gnus-topic-check-topology)
654       (run-hooks 'gnus-topic-mode-hook))
655     ;; Remove topic infestation.
656     (unless gnus-topic-mode
657       (remove-hook 'gnus-summary-exit-hook 'gnus-topic-update-topic)
658       (remove-hook 'gnus-group-change-level-function 
659                    'gnus-topic-change-level)
660       (setq gnus-group-prepare-function 'gnus-group-prepare-flat))
661     (when redisplay
662       (gnus-group-list-groups))))
663     
664 (defun gnus-topic-select-group (&optional all)
665   "Select this newsgroup.
666 No article is selected automatically.
667 If ALL is non-nil, already read articles become readable.
668 If ALL is a number, fetch this number of articles."
669   (interactive "P")
670   (if (gnus-group-topic-p)
671       (let ((gnus-group-list-mode 
672              (if all (cons (if (numberp all) all 7) t) gnus-group-list-mode)))
673         (gnus-topic-fold all))
674     (gnus-group-select-group all)))
675
676 (defun gnus-mouse-pick-topic (e)
677   "Select the group or topic under the mouse pointer."
678   (interactive "e")
679   (mouse-set-point e)
680   (gnus-topic-read-group nil))
681
682 (defun gnus-topic-read-group (&optional all no-article group)
683   "Read news in this newsgroup.
684 If the prefix argument ALL is non-nil, already read articles become
685 readable.  IF ALL is a number, fetch this number of articles.  If the
686 optional argument NO-ARTICLE is non-nil, no article will be
687 auto-selected upon group entry.  If GROUP is non-nil, fetch that
688 group."
689   (interactive "P")
690   (if (gnus-group-topic-p)
691       (let ((gnus-group-list-mode 
692              (if all (cons (if (numberp all) all 7) t) gnus-group-list-mode)))
693         (gnus-topic-fold all))
694     (gnus-group-read-group all no-article group)))
695
696 (defun gnus-topic-create-topic (topic parent &optional previous full-topic)
697   (interactive 
698    (list
699     (read-string "New topic: ")
700     (gnus-group-parent-topic)))
701   ;; Check whether this topic already exists.
702   (when (gnus-topic-find-topology topic)
703     (error "Topic aleady exists"))
704   (unless parent
705     (setq parent (caar gnus-topic-topology)))
706   (let ((top (cdr (gnus-topic-find-topology parent)))
707         (full-topic (or full-topic `((,topic visible)))))
708     (unless top
709       (error "No such parent topic: %s" parent))
710     (if previous
711         (progn
712           (while (and (cdr top)
713                       (not (equal (caaadr top) previous)))
714             (setq top (cdr top)))
715           (setcdr top (cons full-topic (cdr top))))
716       (nconc top (list full-topic)))
717     (unless (assoc topic gnus-topic-alist)
718       (push (list topic) gnus-topic-alist)))
719   (gnus-topic-enter-dribble)
720   (gnus-group-list-groups)
721   (gnus-topic-goto-topic topic))
722
723 (defun gnus-topic-move-group (n topic &optional copyp)
724   "Move the current group to a topic."
725   (interactive
726    (list current-prefix-arg
727          (completing-read "Move to topic: " gnus-topic-alist nil t)))
728   (let ((groups (gnus-group-process-prefix n))
729         (topicl (assoc topic gnus-topic-alist))
730         entry)
731     (mapcar (lambda (g) 
732               (gnus-group-remove-mark g)
733               (when (and
734                      (setq entry (assoc (gnus-group-topic g) gnus-topic-alist))
735                      (not copyp))
736                 (setcdr entry (delete g (cdr entry))))
737               (when topicl
738                 (nconc topicl (list g))))
739             groups)
740     (gnus-group-position-point))
741   (gnus-topic-enter-dribble)
742   (gnus-group-list-groups))
743
744 (defun gnus-topic-remove-group ()
745   "Remove the current group from the topic."
746   (interactive)
747   (let ((topicl (assoc (gnus-group-parent-topic) gnus-topic-alist))
748         (group (gnus-group-group-name))
749         (buffer-read-only nil))
750     (when (and topicl group)
751       (gnus-delete-line)
752       (delq (gnus-group-group-name) topicl))
753     (gnus-group-position-point)))
754
755 (defun gnus-topic-copy-group (n topic)
756   "Copy the current group to a topic."
757   (interactive
758    (list current-prefix-arg
759          (completing-read "Copy to topic: " gnus-topic-alist nil t)))
760   (gnus-topic-move-group n topic t))
761
762 (defun gnus-topic-group-indentation ()
763   (make-string 
764    (* gnus-topic-indent-level
765       (or (save-excursion
766             (gnus-topic-goto-topic (gnus-group-parent-topic))
767             (gnus-group-topic-level)) 0)) ? ))
768
769 (defun gnus-topic-change-level (group level oldlevel)
770   "Run when changing levels to enter/remove groups from topics."
771   (when (and gnus-topic-mode 
772              gnus-topic-alist
773              (not gnus-topic-inhibit-change-level))
774     ;; Remove the group from the topics.
775     (when (and (< oldlevel gnus-level-zombie)
776                (>= level gnus-level-zombie))
777       (let (alist)
778         (when (setq alist (assoc (gnus-group-topic group) gnus-topic-alist))
779           (setcdr alist (delete group (cdr alist))))))
780     ;; If the group is subscribed. then we enter it into the topics.
781     (when (and (< level gnus-level-zombie)
782                (>= oldlevel gnus-level-zombie))
783       (let* ((prev (gnus-group-group-name))
784              (gnus-topic-inhibit-change-level t)
785              (gnus-group-indentation
786               (make-string 
787                (* gnus-topic-indent-level
788                   (or (save-excursion
789                         (gnus-topic-goto-topic (gnus-group-parent-topic))
790                         (gnus-group-topic-level)) 0)) ? ))
791              (yanked (list group))
792              alist talist end)
793         ;; Then we enter the yanked groups into the topics they belong
794         ;; to. 
795         (when (setq alist (assoc (save-excursion
796                                    (forward-line -1)
797                                    (or
798                                     (gnus-group-parent-topic)
799                                     (caar gnus-topic-topology)))
800                                  gnus-topic-alist))
801           (setq talist alist)
802           (when (stringp yanked)
803             (setq yanked (list yanked)))
804           (if (not prev)
805               (nconc alist yanked)
806             (if (not (cdr alist))
807                 (setcdr alist (nconc yanked (cdr alist)))
808               (while (and (not end) (cdr alist))
809                 (when (equal (cadr alist) prev)
810                   (setcdr alist (nconc yanked (cdr alist)))
811                   (setq end t))
812                 (setq alist (cdr alist)))
813               (unless end
814                 (nconc talist yanked))))))
815       (gnus-topic-update-topic))))
816
817 (defun gnus-topic-goto-next-group (group props)
818   "Go to group or the next group after group."
819   (if (null group)
820       (gnus-topic-goto-topic (symbol-name (cadr (memq 'gnus-topic props))))
821     (if (gnus-group-goto-group group)
822         t
823       ;; The group is no longer visible.
824       (let* ((list (assoc (gnus-group-topic group) gnus-topic-alist))
825              (after (cdr (member group (cdr list)))))
826         ;; First try to put point on a group after the current one.
827         (while (and after
828                     (not (gnus-group-goto-group (car after))))
829           (setq after (cdr after)))
830         ;; Then try to put point on a group before point.
831         (unless after
832           (setq after (cdr (member group (reverse (cdr list)))))
833           (while (and after 
834                       (not (gnus-group-goto-group (car after))))
835             (setq after (cdr after))))
836         ;; Finally, just put point on the topic.
837         (unless after
838           (gnus-topic-goto-topic (car list))
839           (setq after nil))
840         t))))
841
842 (defun gnus-topic-kill-group (&optional n discard)
843   "Kill the next N groups."
844   (interactive "P")
845   (if (gnus-group-topic-p)
846       (let ((topic (gnus-group-topic-name)))
847         (gnus-topic-remove-topic nil t)
848         (push (gnus-topic-find-topology topic nil nil gnus-topic-topology)
849               gnus-topic-killed-topics))
850     (gnus-group-kill-group n discard)
851     (gnus-topic-update-topic)))
852   
853 (defun gnus-topic-yank-group (&optional arg)
854   "Yank the last topic."
855   (interactive "p")
856   (if gnus-topic-killed-topics
857       (let ((previous (gnus-group-parent-topic))
858             (item (cdr (pop gnus-topic-killed-topics))))
859         (gnus-topic-create-topic
860          (caar item) (gnus-topic-parent-topic previous) previous
861          item)
862         (gnus-topic-goto-topic (caar item)))
863     (let* ((prev (gnus-group-group-name))
864            (gnus-topic-inhibit-change-level t)
865            (gnus-group-indentation
866             (make-string 
867              (* gnus-topic-indent-level
868                 (or (save-excursion
869                       (gnus-topic-goto-topic (gnus-group-parent-topic))
870                       (gnus-group-topic-level)) 0)) ? ))
871            yanked alist)
872       ;; We first yank the groups the normal way...
873       (setq yanked (gnus-group-yank-group arg))
874       ;; Then we enter the yanked groups into the topics they belong
875       ;; to. 
876       (setq alist (assoc (save-excursion
877                            (forward-line -1)
878                            (gnus-group-parent-topic))
879                          gnus-topic-alist))
880       (when (stringp yanked)
881         (setq yanked (list yanked)))
882       (if (not prev)
883           (nconc alist yanked)
884         (if (not (cdr alist))
885             (setcdr alist (nconc yanked (cdr alist)))
886           (while (cdr alist)
887             (when (equal (cadr alist) prev)
888               (setcdr alist (nconc yanked (cdr alist)))
889               (setq alist nil))
890             (setq alist (cdr alist))))))
891     (gnus-topic-update-topic)))
892
893 (defun gnus-topic-hide-topic ()
894   "Hide all subtopics under the current topic."
895   (interactive)
896   (when (gnus-group-parent-topic)
897     (gnus-topic-goto-topic (gnus-group-parent-topic))
898     (gnus-topic-remove-topic nil nil 'hidden)))
899
900 (defun gnus-topic-show-topic ()
901   "Show the hidden topic."
902   (interactive)
903   (when (gnus-group-topic-p)
904     (gnus-topic-remove-topic t nil 'shown)))
905
906 (defun gnus-topic-mark-topic (topic &optional unmark)
907   "Mark all groups in the topic with the process mark."
908   (interactive (list (gnus-group-parent-topic)))
909   (save-excursion
910     (let ((groups (gnus-topic-find-groups topic 9 t)))
911       (while groups
912         (funcall (if unmark 'gnus-group-remove-mark 'gnus-group-set-mark)
913                  (gnus-info-group (nth 2 (pop groups))))))))
914
915 (defun gnus-topic-unmark-topic (topic &optional unmark)
916   "Remove the process mark from all groups in the topic."
917   (interactive (list (gnus-group-parent-topic)))
918   (gnus-topic-mark-topic topic t))
919
920 (defun gnus-topic-get-new-news-this-topic (&optional n)
921   "Check for new news in the current topic."
922   (interactive "P")
923   (if (not (gnus-group-topic-p))
924       (gnus-group-get-new-news-this-group n)
925     (gnus-topic-mark-topic (gnus-group-topic-name))
926     (gnus-group-get-new-news-this-group)))
927
928 (defun gnus-topic-move-matching (regexp topic &optional copyp)
929   "Move all groups that match REGEXP to some topic."
930   (interactive
931    (let (topic)
932      (nreverse
933       (list
934        (setq topic (completing-read "Move to topic: " gnus-topic-alist nil t))
935        (read-string (format "Move to %s (regexp): " topic))))))
936   (gnus-group-mark-regexp regexp)
937   (gnus-topic-move-group nil topic copyp))
938
939 (defun gnus-topic-copy-matching (regexp topic &optional copyp)
940   "Copy all groups that match REGEXP to some topic."
941   (interactive
942    (let (topic)
943      (nreverse
944       (list
945        (setq topic (completing-read "Copy to topic: " gnus-topic-alist nil t))
946        (read-string (format "Copy to %s (regexp): " topic))))))
947   (gnus-topic-move-matching regexp topic t))
948
949 (defun gnus-topic-delete (topic)
950   "Delete a topic."
951   (interactive (list (gnus-group-topic-name)))
952   (unless topic
953     (error "No topic to be deleted"))
954   (let ((entry (assoc topic gnus-topic-alist))
955         (buffer-read-only nil))
956     (when (cdr entry)
957       (error "Topic not empty"))
958     ;; Delete if visible.
959     (when (gnus-topic-goto-topic topic)
960       (gnus-delete-line))
961     ;; Remove from alist.
962     (setq gnus-topic-alist (delq entry gnus-topic-alist))
963     ;; Remove from topology.
964     (gnus-topic-find-topology topic nil nil 'delete)))
965
966 (defun gnus-topic-rename (old-name new-name)
967   "Rename a topic."
968   (interactive
969    (let ((topic (gnus-group-parent-topic)))
970      (list topic
971            (read-string (format "Rename %s to: " topic)))))
972   (let ((top (gnus-topic-find-topology old-name))
973         (entry (assoc old-name gnus-topic-alist)))
974     (when top
975       (setcar (cadr top) new-name))
976     (when entry 
977       (setcar entry new-name))
978     (gnus-group-list-groups)))
979
980 (defun gnus-topic-indent (&optional unindent)
981   "Indent a topic -- make it a sub-topic of the previous topic.
982 If UNINDENT, remove an indentation."
983   (interactive "P")
984   (if unindent
985       (gnus-topic-unindent)
986     (let* ((topic (gnus-group-parent-topic))
987            (parent (gnus-topic-previous-topic topic)))
988       (unless parent
989         (error "Nothing to indent %s into" topic))
990       (when topic
991         (gnus-topic-goto-topic topic)
992         (gnus-topic-kill-group)
993         (gnus-topic-create-topic
994          topic parent nil (cdr (pop gnus-topic-killed-topics)))
995         (or (gnus-topic-goto-topic topic)
996             (gnus-topic-goto-topic parent))))))
997
998 (defun gnus-topic-unindent ()
999   "Unindent a topic."
1000   (interactive)
1001   (let* ((topic (gnus-group-parent-topic))
1002          (parent (gnus-topic-parent-topic topic))
1003          (grandparent (gnus-topic-parent-topic parent)))
1004     (unless grandparent
1005       (error "Nothing to indent %s into" topic))
1006     (when topic
1007       (gnus-topic-goto-topic topic)
1008       (gnus-topic-kill-group)
1009       (gnus-topic-create-topic
1010        topic grandparent (gnus-topic-next-topic parent)
1011        (cdr (pop gnus-topic-killed-topics)))
1012       (gnus-topic-goto-topic topic))))
1013
1014 (defun gnus-topic-list-active (&optional force)
1015   "List all groups that Gnus knows about in a topicsified fashion.
1016 If FORCE, always re-read the active file."
1017   (interactive "P")
1018   (when force
1019     (gnus-get-killed-groups))
1020   (gnus-topic-grok-active force)
1021   (let ((gnus-topic-topology gnus-topic-active-topology)
1022         (gnus-topic-alist gnus-topic-active-alist)
1023         gnus-killed-list gnus-zombie-list)
1024     (gnus-group-list-groups 9 nil 1)))
1025
1026 (provide 'gnus-topic)
1027
1028 ;;; gnus-topic.el ends here