woman.el 166 KB
Newer Older
Eli Zaretskii's avatar
Eli Zaretskii committed
1 2
;;; woman.el --- browse UN*X manual pages `wo (without) man'

3
;; Copyright (C) 2000, 2002, 2003, 2004, 2005 Free Software Foundation, Inc.
Eli Zaretskii's avatar
Eli Zaretskii committed
4

5
;; Author: Francis J. Wright <F.J.Wright@qmul.ac.uk>
6
;; Maintainer: FSF
7
;; Keywords: help, unix
Eli Zaretskii's avatar
Eli Zaretskii committed
8
;; Adapted-By: Eli Zaretskii <eliz@gnu.org>
9
;; Version: see `woman-version'
10
;; URL: http://centaur.maths.qmul.ac.uk/Emacs/WoMan/
Eli Zaretskii's avatar
Eli Zaretskii committed
11 12 13 14 15 16 17 18 19 20 21 22 23 24 25

;; 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
Lute Kamstra's avatar
Lute Kamstra committed
26 27
;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
;; Boston, MA 02110-1301, USA.
Eli Zaretskii's avatar
Eli Zaretskii committed
28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98

;;; Commentary:

;; WoMan implements a subset of the formatting performed by the Emacs
;; `man' (or `manual-entry') command to format a UN*X manual `page'
;; for display, but without calling any external programs.  It is
;; intended to emulate the whole of the -man macro package, plus those
;; ?roff requests that are most commonly used in man pages.  However,
;; the emulation is modified to include the reformatting done by the
;; Emacs `man' command.  No hyphenation is performed.

;; Advantages

;;   Much more direct, does not require any external programs.
;;   Supports completion on man page names.

;; Disadvantages

;;   Not a complete emulation.  Currently no support for eqn or tbl.
;;   Slightly slower for large man pages (but usually faster for
;;   small- and medium-size pages).

;; This browser works quite well on simple well-written man files.  It
;; works less well on idiosyncratic files that `break the rules' or
;; use the more obscure ?roff requests directly.  Current test results
;; are available in the file woman.status.

;; WoMan supports the use of compressed man files via
;; `auto-compression-mode' by turning it on if necessary.  But you may
;; need to adjust the user option `woman-file-compression-regexp'.

;; Read on for (currently) the only documentation for WoMan!

;; See also the documentation for the WoMan interactive commands and
;; user option variables, all of which begin with the prefix `woman-'.
;; This can be done most easily by loading WoMan and then running the
;; command `woman-mini-help', or selecting the WoMan menu option `Mini
;; Help' when WoMan is running.

;; WoMan is still under development!  Please let me know what doesn't
;; work -- I am adding and improving functionality as testing shows
;; that it is necessary.  See below for guidance on reporting bugs.

;; Recommended use
;; ===============

;; Put this in your .emacs:
;;   (autoload 'woman "woman"
;;             "Decode and browse a UN*X man page." t)
;;   (autoload 'woman-find-file "woman"
;;             "Find, decode and browse a specific UN*X man-page file." t)

;; Then either (1 -- *RECOMMENDED*): If the `MANPATH' environment
;; variable is set then WoMan will use it; otherwise you may need to
;; reset the Lisp variable `woman-manpath', and you may also want to
;; set the Lisp variable `woman-path'.  Please see the online
;; documentation for these variables.  Now you can execute the
;; extended command `woman', enter or select a manual entry topic,
;; using completion, and if necessary select a filename, using
;; completion.  By default, WoMan suggests the word nearest to the
;; cursor in the current buffer as the topic.

;; Or (2): Execute the extended command `woman-find-file' and enter a
;; filename, using completion.  This mode of execution may be useful
;; for temporary files outside the standard UN*X manual directory
;; structure.

;; Or (3): Put the next two sexpr's in your .emacs:
;; (autoload 'woman-dired-find-file "woman"
;;   "In dired, run the WoMan man-page browser on this file." t)
;; (add-hook 'dired-mode-hook
99 100
;;          (lambda ()
;;            (define-key dired-mode-map "W" 'woman-dired-find-file)))
Eli Zaretskii's avatar
Eli Zaretskii committed
101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156
;; and open the directory containing the man page file using dired,
;; put the cursor on the file, and press `W'.

;; In each case, the result should (!) be a buffer in Man mode showing
;; a formatted manual entry.  When called from WoMan, Man mode should
;; work as advertised, but modified where necessary in the context of
;; WoMan.  (However, `Man' will still invoke the standard Emacs
;; manual-browsing facility rather than `WoMan' -- this is
;; intentional!)

;; (By default, WoMan will automatically define the dired keys "W" and
;; "w" when it loads, but only if they are not already defined.  This
;; behaviour is controlled by the user option `woman-dired-keys'.
;; Note that the `dired-x' (dired extra) package binds
;; `dired-copy-filename-as-kill' to the key "w" (as pointed out by Jim
;; Davidson), although "W" appears to be really unused.  The `dired-x'
;; package will over-write the WoMan binding to "w", whereas (by
;; default) WoMan will not overwrite the `dired-x' binding.)

;; The following is based on suggestions by Guy Gascoigne-Piggford and
;; Juanma Barranquero.  If you really want to square the man-woman
;; circle then you might care to define the following bash function in
;; .bashrc:

;;   man() { gnudoit -q '(raise-frame (selected-frame)) (woman' \"$1\" ')' ; }

;; If you use Microsoft COMMAND.COM then you can create a file called
;; man.bat somewhere in your path containing the two lines:

;;   @echo off
;;   gnudoit -q (raise-frame (selected-frame)) (woman \"%1\")

;; and then (e.g. from a command prompt or the Run... option in the
;; Start menu) just execute

;;   man man_page_name


;; Using the `word at point' as a topic suggestion
;; ===============================================

;; By default, the `woman' command uses the word nearest to point in
;; the current buffer as a suggestion for the topic to look up.  The
;; topic must be confirmed or edited in the minibuffer.  This
;; suggestion can be turned off, or `woman' can use the suggested
;; topic without confirmation* if possible, by setting the user-option
;; `woman-topic-at-point' to nil or t respectively.  (Its default
;; value is neither nil nor t, meaning ask for confirmation.)

;; [* Thanks to Benjamin Riefenstahl for suggesting this
;; functionality.]

;; The variable `woman-topic-at-point' can be rebound locally, which
;; may be useful to provide special private key bindings, e.g.

;;  (global-set-key "\C-cw"
157
;;  		  (lambda ()
Eli Zaretskii's avatar
Eli Zaretskii committed
158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272
;;  		    (interactive)
;;  		    (let ((woman-topic-at-point t))
;;  		      (woman)))))


;; Customization, Hooks and Imenu
;; ==============================

;; WoMan supports the GNU Emacs 20+ customization facility, and puts
;; a customization group called `WoMan' in the `Help' group under the
;; top-level `Emacs' group.  In order to be able to customize WoMan
;; without first loading it, add the following sexp to your .emacs:

;;  (defgroup woman nil
;;     "Browse UNIX manual pages `wo (without) man'."
;;     :tag "WoMan" :group 'help :load "woman")


;; WoMan currently runs two hooks: `woman-pre-format-hook' immediately
;; before formatting a buffer and `woman-post-format-hook' immediately
;; after formatting a buffer.  These hooks can be used for special
;; customizations that require code to be executed, etc., although
;; most customization should be possible by setting WoMan user option
;; variables, e.g. in `.emacs' and should NOT require the use of the
;; hooks.  `woman-pre-format-hook' might be appropriate for face
;; customization, whereas `woman-post-format-hook' might be
;; appropriate for installing a dynamic menu using `imenu' (although
;; it is better to use the built-in WoMan imenu support).

;; The WoMan menu provides an option to make a contents menu for the
;; current man page (using imenu).  Alternatively, if you set the
;; variable `woman-imenu' to `t' then WoMan will do it automatically
;; for every man page.  The menu title is the value of the variable
;; `woman-imenu-title', which is "CONTENTS" by default.  By default,
;; the menu shows manual sections and subsections, but you can change
;; this by changing the value of `woman-imenu-generic-expression'.
;; This facility is not yet widely tested and may be fooled by obscure
;; man pages that `break the rules'.

;; WoMan is configured not to replace spaces in an imenu *Completion*
;; buffer.  For further documentation of the use of imenu, such as
;; menu sorting, see the source file imenu.el, which is distributed
;; with GNU Emacs.

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;; Howard Melman made (essentially) the following suggestions, which
;; are slightly different from the expression that I currently use.
;; You may prefer one of Howard's suggestions, which I think assume
;; that `case-fold-search' is `t' (which it is by default):

;; (setq woman-imenu-generic-expression
;;       '((nil "^\\(   \\)?\\([A-Z][A-Z ]+[A-Z]\\)[ \t]*$" 2)))

;; will give support for .SH and .SS, though it won't show the heading
;; name hierarchy.  If you just want .SH in the imenu then use:

;; (setq woman-imenu-generic-expression
;;       '((nil "^\\([A-Z][A-Z ]+[A-Z]\\)[ \t]*$" 1)))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;


;; Vertical spacing and blank lines
;; ================================

;; The number of consecutive blank lines in the formatted buffer
;; should be either 0 or 1.  A blank line should leave a space like
;; .sp 1 (p. 14).  Current policy is to output vertical space only
;; immediately before text is output.


;; Horizontal and vertical spacing and resolution
;; ==============================================

;; WoMan currently assumes 10 characters per inch horizontally, hence
;; a horizontal resolution of 24 basic units, and 5 lines per inch
;; vertically, hence a vertical resolution of 48 basic units.  (nroff
;; uses 240 per inch).


;; The *WoMan-Log* buffer
;; ======================

;; This is modelled on the byte-compiler.  It logs all files formatted
;; by WoMan, and if WoMan finds anything that it cannot handle then it
;; writes a warning to this buffer.  If the variable `woman-show-log'
;; is non-nil (by default it is `nil') then WoMan automatically
;; displays this buffer.  Many WoMan warnings can be completely
;; ignored, because they are reporting the fact that WoMan has ignored
;; requests that it is correct to ignore.  In some future version this
;; level of paranoia will be reduced, but not until WoMan is more
;; reliable.  At present, all warnings should be treated with some
;; suspicion.  Uninterpreted escape sequences are also logged (in some
;; cases).

;; Uninterpreted ?roff requests can optionally be left in the
;; formatted buffer to indicate precisely where they occur by
;; resetting the variable `woman-ignore' to `nil' (by default it is
;; `t').

;; Automatic initiation of woman decoding

;; (Probably not a good idea.  If you use it, be careful!)

;; Put something like this in your .emacs.  The call to
;; set-visited-file-name is to avoid font-locking triggered by
;; automatic major mode selection.

;; (autoload 'woman-decode-region "woman")

;; (setq format-alist
;;       (cons
;;        '(man "UN*X man-page source format" "\\.\\(TH\\|ig\\) "
;; 	     woman-decode-region nil nil
273 274 275
;; 	     (lambda (arg)
;; 		set-visited-file-name
;; 		(file-name-sans-extension buffer-file-name)))))
Eli Zaretskii's avatar
Eli Zaretskii committed
276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334
;;       format-alist))


;; Reporting Bugs
;; ==============

;; If WoMan fails completely, or formats a file incorrectly
;; (i.e. obviously wrongly or significantly differently from man) or
;; inelegantly, then please

;; (a) check that you are running the latest version of woman.el
;;     available from my web site (see above),

;; (b) check that the problem is not already described in the file
;;     woman.status, also available from my web site.

;; If both of the above are true then please email me the entry from
;; the *WoMan-Log* buffer relating to the problem file, together with
;; a brief description of the problem.  Please indicate where you got
;; the source file from, but do not send it to me unless I ask you to!
;; Thanks.  (There is at present no automated bug-reporting facility
;; for WoMan.)

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;; NOTE:

;; CASE-DEPENDENCE OF FILENAMES.  By default, WoMan ignores case in
;; file pathnames only when it seems appropriate.  MS-Windows users
;; who want complete case independence should set the NTEmacs variable
;; `w32-downcase-file-names' to `t' and use all lower case when
;; setting WoMan file paths.

;; (1) INCOMPATIBLE CHANGE!  WoMan no longer uses a persistent topic
;; cache by default.  (It caused too much confusion!)  Explicitly set
;; the variable `woman-cache-filename' to save the cache between Emacs
;; sessions.  This is recommended only if the command `woman' is too
;; slow the first time that it is run in an Emacs session, while it
;; builds its cache in main memory, which MAY be VERY slow.

;; (2) The user option `woman-cache-level' controls the amount of
;; information cached (in main memory and, optionally, saved to disc).

;; (3) UPDATING THE CACHE.  A prefix argument always causes the
;; `woman' command (only) to rebuild its topic cache, and to re-save
;; it to `woman-cache-filename' if this variable has a non-nil value.
;; This is necessary if the NAMES (not contents) of any of the
;; directories or files in the paths specified by `woman-manpath' or
;; `woman-path' change.  If WoMan user options that affect the cache
;; are changed then WoMan will automatically update its cache file on
;; disc (if one is in use) the next time it is run in a new Emacs
;; session.

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;


;; TO DO
;; =====

335
;; Reconsider case sensitivity of file names.
Eli Zaretskii's avatar
Eli Zaretskii committed
336 337 338 339
;; MUST PROCESS .if, .nr IN ORDER ENCOUNTERED IN FILE! (rcsfile, mf).
;; Allow general delimiter in `\v', cf. `\h'.
;; Improve major-mode documentation.
;; Pre-process conditionals in macro bodies if possible for speed?
340 341
;; Emulate more complete preprocessor support for tbl (.TS/.TE)
;; Emulate some preprocessor support for eqn (.EQ/.EN)
Eli Zaretskii's avatar
Eli Zaretskii committed
342 343 344 345 346 347 348 349 350 351 352 353 354 355 356
;; Re-write filling and adjusting code!
;; Allow word wrap at comma (for long option lists)?
;; Buffer list handling not quite right.
;; Make 10 or 12 pitch (cpi) optional -- 12 => ll = 78
;; Use unpaddable space for tabbing?
;; Tidy up handling of fonts when filling and adjusting
;;   -- see text/text properties?
;; Improve speed
;; Add font-lock support (for quoted strings, etc.)?
;; Optionally save large files in enriched format?
;; Add apropos facility by searching NAME (?) entry in man files?
;; Documentation -- optional auto-display of formatted WoMan man page?
;; Implement a bug reporter?
;; Support diversion and traps (to some extent) - for Tcl/tk pages?
;; Add a menu of WoMan buffers?
357
;; Fix .fc properly?
Eli Zaretskii's avatar
Eli Zaretskii committed
358 359 360 361 362 363 364 365 366 367 368 369 370 371


;; Implementation strategy [this description is now well out of date!]
;; -- three main passes, each to process respectively:

;;   1) non-breaking `.' requests including font macros
;;   2) \ escape sequences, mainly special characters and font changes
;;   3) breaking `.' requests, mainly filling and justification

;; For each pass, a control function finds and pre-processes the
;; escape or request and then calls the appropriate function to
;; perform the required formatting.  Based originally on enriched.el
;; and format.el.

372 373 374
;; The background information that made this project possible is
;; freely available courtesy of Bell Labs from
;; http://cm.bell-labs.com/7thEdMan/
Eli Zaretskii's avatar
Eli Zaretskii committed
375 376 377 378 379 380 381 382 383 384 385 386 387 388 389


;; Acknowledgements
;; ================

;; For Heather, Kathryn and Madelyn, the women in my life
;; (although they will probably never use it)!

;; I also thank the following for helpful suggestions, bug reports,
;; code fragments, general interest, etc.:
;;   Jari Aalto <jari.aalto@cs.tpu.fi>
;;   Dean Andrews <dean@dra.com>
;;   Juanma Barranquero <barranquero@laley-actualidad.es>
;;   Karl Berry <kb@cs.umb.edu>
;;   Jim Chapman <jchapman@netcomuk.co.uk>
390
;;   Kin Cho <kin@neoscale.com>
Eli Zaretskii's avatar
Eli Zaretskii committed
391 392 393 394 395 396 397 398 399 400 401 402 403 404
;;   Frederic Corne <frederic.corne@erli.fr>
;;   Peter Craft <craft@alacritech.com>
;;   Charles Curley <ccurley@trib.com>
;;   Jim Davidson <jdavidso@teknowledge.com>
;;   Kevin D'Elia <Kevin.DElia@mci.com>
;;   John Fitch <jpff@maths.bath.ac.uk>
;;   Hans Frosch <jwfrosch@rish.b17c.ingr.com>
;;   Guy Gascoigne-Piggford <ggp@informix.com>
;;   Brian Gorka <gorkab@sanchez.com>
;;   Nicolai Henriksen <nhe@lyngso-industri.dk>
;;   Thomas Herchenroeder <the@software-ag.de>
;;   Alexander Hinds <ahinds@thegrid.net>
;;   Stefan Hornburg <sth@hacon.de>
;;   Theodore Jump <tjump@cais.com>
405
;;   David Kastrup <dak@gnu.org>
Eli Zaretskii's avatar
Eli Zaretskii committed
406 407 408 409 410 411 412 413 414 415 416 417 418 419 420
;;   Paul Kinnucan <paulk@mathworks.com>
;;   Jonas Linde <jonas@init.se>
;;   Andrew McRae <andrewm@optimation.co.nz>
;;   Howard Melman <howard@silverstream.com>
;;   Dennis Pixton <dennis@math.binghamton.edu>
;;   T. V. Raman <raman@Adobe.COM>
;;   Bruce Ravel <bruce.ravel@nist.gov>
;;   Benjamin Riefenstahl <benny@crocodial.de>
;;   Kevin Ruland <kruland@seistl.com>
;;   Tom Schutter <tom@platte.com>
;;   Wei-Xue Shi <wxshi@ma.neweb.ne.jp>
;;   Fabio Somenzi <fabio@joplin.colorado.edu>
;;   Karel Sprenger <ks@ic.uva.nl>
;;   Chris Szurgot <szurgot@itribe.net>
;;   Paul A. Thompson <pat@po.cwru.edu>
421
;;   Arrigo Triulzi <arrigo@maths.qmw.ac.uk>
Eli Zaretskii's avatar
Eli Zaretskii committed
422
;;   Geoff Voelker <voelker@cs.washington.edu>
Eli Zaretskii's avatar
Eli Zaretskii committed
423
;;   Eli Zaretskii <eliz@gnu.org>
Eli Zaretskii's avatar
Eli Zaretskii committed
424

425 426
;;; History:
;;  For recent change log see end of file.
Eli Zaretskii's avatar
Eli Zaretskii committed
427 428 429 430


;;; Code:

431 432
(defvar woman-version "0.551 (beta)" "WoMan version information.")

Eli Zaretskii's avatar
Eli Zaretskii committed
433 434 435 436 437
(require 'man)
(eval-when-compile			; to avoid compiler warnings
  (require 'dired)
  (require 'apropos))

438
(defun woman-mapcan (fn x)
439
  "Return concatenated list of FN applied to successive `car' elements of X.
440 441
FN must return a list, cons or nil.  Useful for splicing into a list."
  ;; Based on the Standard Lisp function MAPCAN but with args swapped!
442 443
  ;; More concise implementation than the recursive one.  -- dak
  (apply #'nconc (mapcar fn x)))
444

445 446 447
(defun woman-parse-colon-path (paths)
  "Explode search path string PATHS into a list of directory names.
Allow Cygwin colon-separated search paths on Microsoft platforms.
448
Replace null components by calling `woman-parse-man.conf'.
449 450
As a special case, if PATHS is nil then replace it by calling
`woman-parse-man.conf'."
451
  ;; Based on suggestions by Jari Aalto and Eli Zaretskii.
452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500
  ;; parse-colon-path returns nil for a null path component and
  ;; an empty substring of MANPATH denotes the default list.
  (if (memq system-type '(windows-nt ms-dos))
      (cond ((null paths)
	     (mapcar 'woman-Cyg-to-Win (woman-parse-man.conf)))
	    ((string-match ";" paths)
	     ;; Assume DOS-style path-list...
	     (woman-mapcan		; splice list into list
	      (lambda (x)
		(if x
		    (list x)
		  (mapcar 'woman-Cyg-to-Win (woman-parse-man.conf))))
	      (parse-colon-path paths)))
	    ((string-match "\\`[a-zA-Z]:" paths)
	     ;; Assume single DOS-style path...
	     paths)
	    (t
	     ;; Assume UNIX/Cygwin-style path-list...
	     (woman-mapcan		; splice list into list
	      (lambda (x)
		(mapcar 'woman-Cyg-to-Win
			(if x (list x) (woman-parse-man.conf))))
	      (let ((path-separator ":"))
		(parse-colon-path paths)))))
    ;; Assume host-default-style path-list...
    (woman-mapcan			; splice list into list
     (lambda (x) (if x (list x) (woman-parse-man.conf)))
     (parse-colon-path (or paths "")))))

(defun woman-Cyg-to-Win (file)
  "Convert an absolute filename FILE from Cygwin to Windows form."
  ;; Code taken from w32-symlinks.el
  (if (eq (aref file 0) ?/)
      ;; Try to use Cygwin mount table via `cygpath.exe'.
      (condition-case nil
	  (with-temp-buffer
	    ;; cygpath -m file
	    (call-process "cygpath" nil t nil "-m" file)
	    (buffer-substring 1 (buffer-size)))
	(error
	 ;; Assume no `cygpath' program available.
	 ;; Hack /cygdrive/x/ or /x/ or (obsolete) //x/ to x:/
	 (when (string-match "\\`\\(/cygdrive\\|/\\)?/./" file)
	   (if (match-string 1)		; /cygdrive/x/ or //x/ -> /x/
	       (setq file (substring file (match-end 1))))
	   (aset file 0 (aref file 1))	; /x/ -> xx/
	   (aset file 1 ?:))		; xx/ -> x:/
	 file))
    file))
Eli Zaretskii's avatar
Eli Zaretskii committed
501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537


;;; User options:

;; NB: Group identifiers must be lowercase!

(defgroup woman nil
  "Browse UNIX manual pages `wo (without) man'."
  :tag "WoMan"
  :group 'help)

(defcustom woman-show-log nil
  "*If non-nil then show the *WoMan-Log* buffer if appropriate.
I.e. if any warning messages are written to it.  Default is nil."
  :type 'boolean
  :group 'woman)

(defcustom woman-pre-format-hook nil
  "*Hook run by WoMan immediately before formatting a buffer.
Change only via `Customization' or the function `add-hook'."
  :type 'hook
  :group 'woman)

(defcustom woman-post-format-hook nil
  "*Hook run by WoMan immediately after formatting a buffer.
Change only via `Customization' or the function `add-hook'."
  :type 'hook
  :group 'woman)


;; Interface options

(defgroup woman-interface nil
  "Interface options for browsing UNIX manual pages `wo (without) man'."
  :tag "WoMan Interface"
  :group 'woman)

538
(defcustom woman-man.conf-path
539 540 541 542
  (let ((path '("/usr/lib" "/etc")))
    (if (eq system-type 'windows-nt)
	(mapcar 'woman-Cyg-to-Win path)
      path))
543
  "*List of dirs to search and/or files to try for man config file.
544 545 546
A trailing separator (`/' for UNIX etc.) on directories is optional,
and the filename is used if a directory specified is the first to
contain the strings \"man\" and \".conf\" (in that order).
547 548 549 550 551 552
If MANPATH is not set but a config file is found then it is parsed
instead to provide a default value for `woman-manpath'."
  :type '(repeat string)
  :group 'woman-interface)

(defun woman-parse-man.conf ()
553
  "Parse if possible configuration file for man command.
554
Used only if MANPATH is not set or contains null components.
555 556
Look in `woman-man.conf-path' and return a value for `woman-manpath'.
Concatenate data from all lines in the config file of the form
557
  MANPATH  /usr/man
558
or
559 560 561
  MANDATORY_MANPATH  /usr/man
or
  OPTIONAL_MANPATH  /usr/man"
562 563 564 565 566 567 568 569 570 571 572
  ;; Functionality suggested by Charles Curley.
  (let ((path woman-man.conf-path)
	file manpath)
    (while (and
	    path
	    (not (and
		  (file-readable-p (setq file (car path)))
		  ;; If not a file then find the file:
		  (or (not (file-directory-p file))
		      (and
		       (setq file
573
			     (directory-files file t "man.*\\.conf" t))
574 575 576 577 578
		       (file-readable-p (setq file (car file)))))
		  ;; Parse the file -- if no MANPATH data ignore it:
		  (with-temp-buffer
		    (insert-file-contents file)
		    (while (re-search-forward
579 580
			    ;; `\(?: ... \)' is a "shy group"
			    "\
581
^[ \t]*\\(?:MANDATORY_\\|OPTIONAL_\\)?MANPATH[ \t]+\\(\\S-+\\)" nil t)
582
		      (setq manpath (cons (match-string 1) manpath)))
583 584 585 586 587
		    manpath))
		 ))
      (setq path (cdr path)))
    (nreverse manpath)))

Eli Zaretskii's avatar
Eli Zaretskii committed
588
(defcustom woman-manpath
589 590
  (or (woman-parse-colon-path (getenv "MANPATH"))
      '("/usr/man" "/usr/share/man" "/usr/local/man"))
Eli Zaretskii's avatar
Eli Zaretskii committed
591 592 593 594
  "*List of DIRECTORY TREES to search for UN*X manual files.
Each element should be the name of a directory that contains
subdirectories of the form `man?', or more precisely subdirectories
selected by the value of `woman-manpath-man-regexp'.  Non-directory
595 596 597 598
and unreadable files are ignored.

If not set then the environment variable MANPATH is used.  If no such
environment variable is found, the default list is determined by
599 600 601
consulting the man configuration file if found, which is determined by
the user option `woman-man.conf-path'.  An empty substring of MANPATH
denotes the default list.
Eli Zaretskii's avatar
Eli Zaretskii committed
602

603
Any environment variables (names must have the UN*X-style form $NAME,
604
e.g. $HOME, $EMACSDATA, $emacs_dir) are evaluated first but each
Eli Zaretskii's avatar
Eli Zaretskii committed
605 606 607 608 609 610
element must evaluate to a SINGLE directory name.  Trailing `/'s are
ignored.  (Specific directories in `woman-path' are also searched.)

Microsoft platforms:
I recommend including drive letters explicitly, e.g.

611
  (\"C:/Cygwin/usr/man/\" \"C:/Cygwin/usr/local/man\").
Eli Zaretskii's avatar
Eli Zaretskii committed
612 613

The MANPATH environment variable may be set using DOS semi-colon-
614
separated or UN*X/Cygwin colon-separated syntax (but not mixed)."
Eli Zaretskii's avatar
Eli Zaretskii committed
615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642
  :type '(repeat string)
  :group 'woman-interface)

(defcustom woman-manpath-man-regexp "[Mm][Aa][Nn]"
  "Regexp to match man directories UNDER `woman-manpath' directories.
These normally have names of the form `man?'.  Its default value is
\"[Mm][Aa][Nn]\", which is case-insensitive mainly for the benefit of
Microsoft platforms.  Its purpose is to avoid `cat?', `.', `..', etc."
  ;; Based on a suggestion by Wei-Xue Shi.
  :type 'string
  :group 'woman-interface)

(defcustom woman-path
  (if (eq system-type 'ms-dos) '("$DJDIR/info" "$DJDIR/man/cat[1-9onlp]"))
  "*List of SPECIFIC DIRECTORIES to search for UN*X manual files.
For example

  (\"/emacs/etc\").

These directories are searched in addition to the directory trees
specified in `woman-manpath'.  Each element should be a directory
string or nil, which represents the current directory when the path is
expanded and cached.  However, the last component (only) of each
directory string is treated as a regexp \(Emacs, not shell) and the
string is expanded into a list of matching directories.  Non-directory
and unreadable files are ignored.  The default value is nil.

Any environment variables (which must have the UN*X-style form $NAME,
643
e.g. $HOME, $EMACSDATA, $emacs_dir) are evaluated first but each
Eli Zaretskii's avatar
Eli Zaretskii committed
644 645 646
element must evaluate to a SINGLE directory name (regexp, see above).
For example

647
  (\"$EMACSDATA\") [or equivalently (\"$emacs_dir/etc\")].
Eli Zaretskii's avatar
Eli Zaretskii committed
648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705

Trailing `/'s are discarded.  (The directory trees in `woman-manpath'
are also searched.)  On Microsoft platforms I recommend including
drive letters explicitly."
  :type '(repeat (choice string (const nil)))
  :group 'woman-interface)

(defcustom woman-cache-level 2
  "*The level of topic caching.
1 - cache only the topic and directory lists
    (the only level before version 0.34 - only for compatibility);
2 - cache also the directories for each topic
    (faster, without using much more memory);
3 - cache also the actual filenames for each topic
    (fastest, but uses twice as much memory).
The default value is currently 2, a good general compromise.
If the `woman' command is slow to find files then try 3, which may be
particularly beneficial with large remote-mounted man directories.
Run the `woman' command with a prefix argument or delete the cache
file `woman-cache-filename' for a change to take effect.
\(Values < 1 behave like 1; values > 3 behave like 3.)"
  :type '(choice (const :tag "Minimal" 1)
		 (const :tag "Default" 2)
		 (const :tag "Maximal" 3))
  :group 'woman-interface)

(defcustom woman-cache-filename nil
  "*The full pathname of the WoMan directory and topic cache file.
It is used to save and restore the cache between sessions.  This is
especially useful with remote-mounted man page files!  The default
value of nil suppresses this action.  The `standard' non-nil
filename is \"~/.wmncach.el\".  Remember that a prefix argument forces
the `woman' command to update and re-write the cache."
  :type '(choice (const :tag "None" nil)
		 (const :tag "~/.wmncach.el" "~/.wmncach.el")
		 file)
  :group 'woman-interface)

(defcustom woman-dired-keys t
  "*List of `dired' mode keys to define to run WoMan on current file.
E.g. '(\"w\" \"W\"), or any non-null atom to automatically define
\"w\" and \"W\" if they are unbound, or nil to do nothing.
Default is t."
  :type '(choice (const :tag "None" nil)
		 (repeat string)
		 (other :tag "Auto" t))
  :group 'woman-interface)

(defcustom woman-imenu-generic-expression
  '((nil "\n\\([A-Z].*\\)" 1) ; SECTION, but not TITLE
    ("*Subsections*" "^   \\([A-Z].*\\)" 1))
  "*Imenu support for Sections and Subsections.
An alist with elements of the form (MENU-TITLE REGEXP INDEX) --
see the documentation for `imenu-generic-expression'."
  :type 'sexp
  :group 'woman-interface)

(defcustom woman-imenu nil
706 707
  "*If non-nil then WoMan adds a Contents menu to the menubar.
It does this by calling `imenu-add-to-menubar'.  Default is nil."
Eli Zaretskii's avatar
Eli Zaretskii committed
708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749
  :type 'boolean
  :group 'woman-interface)

(defcustom woman-imenu-title "CONTENTS"
  "*The title to use if WoMan adds a Contents menu to the menubar.
Default is \"CONTENTS\"."
  :type 'string
  :group 'woman-interface)

(defcustom woman-topic-at-point-default 'confirm
  ;; `woman-topic-at-point' may be let-bound when woman is loaded, in
  ;; which case its global value does not get defined.
  ;; `woman-file-name' sets it to this value if it is unbound.
  "*Default value for `woman-topic-at-point'."
  :type '(choice (const :tag "Yes" t)
		 (const :tag "No" nil)
		 (other :tag "Confirm" confirm))
  :group 'woman-interface)

(defcustom woman-topic-at-point woman-topic-at-point-default
  "*Controls use by `woman' of `word at point' as a topic suggestion.
If non-nil then the `woman' command uses the word at point as an
initial topic suggestion when it reads a topic from the minibuffer; if
t then the `woman' command uses the word at point WITHOUT
INTERACTIVE CONFIRMATION if it exists as a topic.  The default value
is `confirm', meaning suggest a topic and ask for confirmation."
  :type '(choice (const :tag "Yes" t)
		 (const :tag "No" nil)
		 (other :tag "Confirm" confirm))
  :group 'woman-interface)

(defvar woman-file-regexp nil
  "Regexp used to select (possibly compressed) man source files, e.g.
\"\\.\\([0-9lmnt]\\w*\\)\\(\\.\\(g?z\\|bz2\\)\\)?\\'\".
Built automatically from the customizable user options
`woman-uncompressed-file-regexp' and `woman-file-compression-regexp'.")

(defvar woman-uncompressed-file-regexp)	; for the compiler
(defvar woman-file-compression-regexp)	; for the compiler

(defun set-woman-file-regexp (symbol value)
  "Bind SYMBOL to VALUE and set `woman-file-regexp' as per user customizations.
750
Used as :set cookie by Customize when customizing the user options
Eli Zaretskii's avatar
Eli Zaretskii committed
751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779
`woman-uncompressed-file-regexp' and `woman-file-compression-regexp'."
  (set-default symbol value)
  (and (boundp 'woman-uncompressed-file-regexp)
       (boundp 'woman-file-compression-regexp)
       (setq woman-file-regexp
	     (concat woman-uncompressed-file-regexp
		     "\\("
		     (substring woman-file-compression-regexp 0 -2)
		     "\\)?\\'"))))

(defcustom woman-uncompressed-file-regexp
  "\\.\\([0-9lmnt]\\w*\\)"		; disallow no extension
  "*Do not change this unless you are sure you know what you are doing!
Regexp used to select man source files (ignoring any compression extension).

The SysV standard man pages use two character suffixes, and this is
becoming more common in the GNU world.  For example, the man pages
in the ncurses package include `toe.1m', `form.3x', etc.

Note: an optional compression regexp will be appended, so this regexp
MUST NOT end with any kind of string terminator such as $ or \\'."
  :type 'regexp
  :set 'set-woman-file-regexp
  :group 'woman-interface)

(defcustom woman-file-compression-regexp
  "\\.\\(g?z\\|bz2\\)\\'"
  "*Do not change this unless you are sure you know what you are doing!
Regexp used to match compressed man file extensions for which
780
decompressors are available and handled by auto-compression mode,
Eli Zaretskii's avatar
Eli Zaretskii committed
781 782
e.g. \"\\\\.\\\\(g?z\\\\|bz2\\\\)\\\\'\" for `gzip' or `bzip2'.
Should begin with \\. and end with \\' and MUST NOT be optional."
783 784 785 786
  ;; Should be compatible with car of
  ;; `jka-compr-file-name-handler-entry', but that is unduly
  ;; complicated, includes an inappropriate extension (.tgz) and is
  ;; not loaded by default!
Eli Zaretskii's avatar
Eli Zaretskii committed
787 788 789 790
  :type 'regexp
  :set 'set-woman-file-regexp
  :group 'woman-interface)

791 792 793
(defcustom woman-use-own-frame		; window-system
  (or (and (fboundp 'display-graphic-p) (display-graphic-p)) ; Emacs 21
      (memq window-system '(x w32)))	; Emacs 20
794 795 796 797 798
  "*If non-nil then use a dedicated frame for displaying WoMan windows.
Only useful when run on a graphic display such as X or MS-Windows."
  :type 'boolean
  :group 'woman-interface)

Eli Zaretskii's avatar
Eli Zaretskii committed
799 800 801 802 803 804 805 806 807 808 809 810 811 812 813

;; Formatting options

(defgroup woman-formatting nil
  "Formatting options for browsing UNIX manual pages `wo (without) man'."
  :tag "WoMan Formatting"
  :group 'woman)

(defcustom woman-fill-column 65
  "*Right margin for formatted text -- default is 65."
  :type 'integer
  :group 'woman-formatting)

(defcustom woman-fill-frame nil
  ;; Based loosely on a suggestion by Theodore Jump:
Juri Linkov's avatar
Juri Linkov committed
814
  "*If non-nil then most of the window width is used."
Eli Zaretskii's avatar
Eli Zaretskii committed
815 816 817 818 819
  :type 'boolean
  :group 'woman-formatting)

(defcustom woman-default-indent 5
  "*Default prevailing indent set by -man macros -- default is 5.
820
Set this variable to 7 to emulate GNU man formatting."
Eli Zaretskii's avatar
Eli Zaretskii committed
821 822 823 824 825
  :type 'integer
  :group 'woman-formatting)

(defcustom woman-bold-headings t
  "*If non-nil then embolden section and subsection headings.  Default is t.
826
Heading emboldening is NOT standard `man' behavior."
Eli Zaretskii's avatar
Eli Zaretskii committed
827 828 829 830
  :type 'boolean
  :group 'woman-formatting)

(defcustom woman-ignore t
831
  "*If non-nil then unrecognized requests etc. are ignored.  Default is t.
832
This gives the standard ?roff behavior.  If nil then they are left in
Eli Zaretskii's avatar
Eli Zaretskii committed
833 834 835 836
the buffer, which may aid debugging."
  :type 'boolean
  :group 'woman-formatting)

837 838 839 840 841 842 843 844 845
(defcustom woman-preserve-ascii t
  "*If non-nil, preserve ASCII characters in the WoMan buffer.
Otherwise, to save time, some backslashes and spaces may be
represented differently (as the values of the variables
`woman-escaped-escape-char' and `woman-unpadded-space-char'
respectively) so that the buffer content is strictly wrong even though
it should display correctly.  This should be irrelevant unless the
buffer text is searched, copied or saved to a file."
  ;; This option should probably be removed!
Eli Zaretskii's avatar
Eli Zaretskii committed
846 847 848
  :type 'boolean
  :group 'woman-formatting)

849 850 851 852 853 854 855
(defcustom woman-emulation 'nroff
  "*WoMan emulation, currently either nroff or troff.  Default is nroff.
Troff emulation is experimental and largely untested.
\(Add groff later?)"
  :type '(choice (const nroff) (const troff))
  :group 'woman-formatting)

Eli Zaretskii's avatar
Eli Zaretskii committed
856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874

;; Faces:

(defgroup woman-faces nil
  "Face options for browsing UNIX manual pages `wo (without) man'."
  :tag "WoMan Faces"
  :group 'woman
  :group 'faces)

(defcustom woman-fontify
  (or (and (fboundp 'display-color-p) (display-color-p))
      (and (fboundp 'display-graphic-p) (display-graphic-p))
      (x-display-color-p))
  "*If non-nil then WoMan assumes that face support is available.
It defaults to a non-nil value if the display supports either colors
or different fonts."
  :type 'boolean
  :group 'woman-faces)

875 876 877
;; This is overkill!  Troff uses just italic; Nroff uses just underline.
;; You should probably select either italic or underline as you prefer, but
;; not both, although italic and underline work together perfectly well!
878
(defface woman-italic
879
  `((((min-colors 88) (background light))
880 881
     (:slant italic :underline t :foreground "red1"))
    (((background light)) (:slant italic :underline t :foreground "red"))
882
    (((background dark)) (:slant italic :underline t)))
883
  "Face for italic font in man pages."
Eli Zaretskii's avatar
Eli Zaretskii committed
884
  :group 'woman-faces)
885 886
;; backward-compatibility alias
(put 'woman-italic-face 'face-alias 'woman-italic)
Eli Zaretskii's avatar
Eli Zaretskii committed
887

888
(defface woman-bold
889 890
  '((((min-colors 88) (background light)) (:weight bold :foreground "blue1"))
    (((background light)) (:weight bold :foreground "blue"))
891
    (((background dark)) (:weight bold :foreground "green2")))
892
  "Face for bold font in man pages."
Eli Zaretskii's avatar
Eli Zaretskii committed
893
  :group 'woman-faces)
894 895
;; backward-compatibility alias
(put 'woman-bold-face 'face-alias 'woman-bold)
Eli Zaretskii's avatar
Eli Zaretskii committed
896

897 898 899
;; Brown is a good compromise: it is distinguishable from the default
;; but not enough so to make font errors look terrible.  (Files that use
;; non-standard fonts seem to do so badly or in idiosyncratic ways!)
900
(defface woman-unknown
901
  '((((background light)) (:foreground "brown"))
902
    (((min-colors 88) (background dark)) (:foreground "cyan1"))
903 904
    (((background dark)) (:foreground "cyan")))
  "Face for all unknown fonts in man pages."
Eli Zaretskii's avatar
Eli Zaretskii committed
905
  :group 'woman-faces)
906 907
;; backward-compatibility alias
(put 'woman-unknown-face 'face-alias 'woman-unknown)
Eli Zaretskii's avatar
Eli Zaretskii committed
908

909
(defface woman-addition
Eli Zaretskii's avatar
Eli Zaretskii committed
910
  '((t (:foreground "orange")))
911
  "Face for all WoMan additions to man pages."
Eli Zaretskii's avatar
Eli Zaretskii committed
912
  :group 'woman-faces)
913 914
;; backward-compatibility alias
(put 'woman-addition-face 'face-alias 'woman-addition)
Eli Zaretskii's avatar
Eli Zaretskii committed
915

916
(defun woman-default-faces ()
917
  "Set foreground colors of italic and bold faces to their default values."
Eli Zaretskii's avatar
Eli Zaretskii committed
918
  (interactive)
919 920
  (face-spec-set 'woman-italic (face-user-default-spec 'woman-italic))
  (face-spec-set 'woman-bold (face-user-default-spec 'woman-bold)))
Eli Zaretskii's avatar
Eli Zaretskii committed
921

922
(defun woman-monochrome-faces ()
923
  "Set foreground colors of italic and bold faces to that of the default face.
924
This is usually either black or white."
Eli Zaretskii's avatar
Eli Zaretskii committed
925
  (interactive)
926 927
  (set-face-foreground 'woman-italic 'unspecified)
  (set-face-foreground 'woman-bold 'unspecified))
Eli Zaretskii's avatar
Eli Zaretskii committed
928 929

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
930 931
;; Experimental font support, initially only for MS-Windows.
(defconst woman-font-support
932
  (eq window-system 'w32)		; Support X later!
933 934 935 936 937 938 939 940 941 942 943 944 945 946 947
  "If non-nil then non-ASCII characters and symbol font supported.")

(defun woman-select-symbol-fonts (fonts)
  "Select symbol fonts from a list FONTS of font name strings."
  (let (symbol-fonts)
    ;; With NTEmacs 20.5, the PATTERN option to `x-list-fonts' does
    ;; not seem to work and fonts may be repeated, so ...
    (while fonts
      (and (string-match "-Symbol-" (car fonts))
	   (not (member (car fonts) symbol-fonts))
	   (setq symbol-fonts (cons (car fonts) symbol-fonts)))
      (setq fonts (cdr fonts)))
    symbol-fonts))

(when woman-font-support
948
  (make-face 'woman-symbol)
Eli Zaretskii's avatar
Eli Zaretskii committed
949

950
  ;; Set the symbol font only if `woman-use-symbol-font' is true, to
Eli Zaretskii's avatar
Eli Zaretskii committed
951 952
  ;; avoid unnecessarily upsetting the line spacing in NTEmacs 20.5!

953 954 955 956 957 958 959 960
  (defcustom woman-use-extended-font t
    "*If non-nil then may use non-ASCII characters from the default font."
    :type 'boolean
    :group 'woman-faces)

  (defcustom woman-use-symbol-font nil
    "*If non-nil then may use the symbol font.  It is off by default,
mainly because it may change the line spacing (in NTEmacs 20.5)."
Eli Zaretskii's avatar
Eli Zaretskii committed
961 962 963 964
    :type 'boolean
    :group 'woman-faces)

  (defconst woman-symbol-font-list
965 966 967
    (or (woman-select-symbol-fonts (x-list-fonts "*" 'default))
	(woman-select-symbol-fonts (x-list-fonts "*")))
    "Symbol font(s), preferably same size as default when WoMan was loaded.")
Eli Zaretskii's avatar
Eli Zaretskii committed
968 969 970 971 972 973 974

  (defcustom woman-symbol-font (car woman-symbol-font-list)
    "*A string describing the symbol font to use for special characters.
It should be compatible with, and the same size as, the default text font.
Under MS-Windows, the default is
  \"-*-Symbol-normal-r-*-*-*-*-96-96-p-*-ms-symbol\"."
    :type `(choice
975
	    ,@(mapcar (lambda (x) (list 'const x))
Eli Zaretskii's avatar
Eli Zaretskii committed
976 977 978 979 980 981
		      woman-symbol-font-list)
	    string)
    :group 'woman-faces)

  )

982 983 984 985
;; For non windows-nt ...
(defvar woman-use-extended-font nil)
(defvar woman-use-symbol-font nil)
(defvar woman-symbol-font nil)
Eli Zaretskii's avatar
Eli Zaretskii committed
986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;


;;; Internal variables:

(defconst woman-justify-list
  '(left right center full)
  "Justify styles for `fill-region-as-paragraph'.")
(defconst woman-adjust-left 0		; == adjust off, noadjust
  "Adjustment indicator `l' -- adjust left margin only.")
(defconst woman-adjust-right 1
  "Adjustment indicator `r' -- adjust right margin only.")
(defconst woman-adjust-center 2
  "Adjustment indicator `c' -- center.")
(defconst woman-adjust-both 3		; default -- adj,both
  "Adjustment indicator `b' or `n' -- adjust both margins.")

(defvar woman-adjust woman-adjust-both
  "Current adjustment number-register value.")
(defvar woman-adjust-previous woman-adjust
  "Previous adjustment number-register value.")
(defvar woman-justify
  (nth woman-adjust woman-justify-list)	; use vector?
  "Current justification style for `fill-region-as-paragraph'.")
(defvar woman-justify-previous woman-justify
  "Previous justification style for `fill-region-as-paragraph'.")

(defvar woman-left-margin woman-default-indent
  "Current left margin.")
(defvar woman-prevailing-indent woman-default-indent
  "Current prevailing indent.")
(defvar woman-interparagraph-distance 1
  "Interparagraph distance in lines.
Set by .PD; used by .SH, .SS, .TP, .LP, .PP, .P, .IP, .HP.")
(defvar woman-leave-blank-lines nil
  "Blank lines to leave as vertical space.")
(defconst woman-tab-width 5
  "Default tab width set by -man macros.")
(defvar woman-nofill nil
  "Current fill mode: nil for filling.")
(defvar woman-RS-left-margin nil
  "Left margin stack for nested use of `.RS/.RE'.")
(defvar woman-RS-prevailing-indent nil
  "Prevailing indent stack for nested use of `.RS/.RE'.")
(defvar woman-nospace nil
  "Current no-space mode: nil for normal spacing.
Set by `.ns' request; reset by any output or `.rs' request")

(defsubst woman-reset-nospace ()
1035
  "Set `woman-nospace' to nil."
Eli Zaretskii's avatar
Eli Zaretskii committed
1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046
  (setq woman-nospace nil))

(defconst woman-request-regexp "^[.'][ \t]*\\(\\S +\\) *"
  ;; Was "^\\.[ \t]*\\([a-z0-9]+\\) *" but cvs.1 uses a macro named
  ;; "`" and CGI.man uses a macro named "''"!
  ;; CGI.man uses ' as control character in places -- it *should*
  ;; suppress breaks!
  ;; Could end with "\\( +\\|$\\)" instead of " *"
  "Regexp to match a ?roff request plus trailing white space.")

(defvar woman-imenu-done nil
1047
  "Buffer-local: set to true if function `woman-imenu' has been called.")
Eli Zaretskii's avatar
Eli Zaretskii committed
1048 1049 1050 1051 1052 1053 1054 1055 1056
(make-variable-buffer-local 'woman-imenu-done)

;; From imenu.el -- needed when reformatting a file in its old buffer.
;; The latest buffer index used to update the menu bar menu.
(eval-when-compile
  (require 'imenu))
(make-variable-buffer-local 'imenu--last-menubar-index-alist)

(defvar woman-buffer-alist nil
1057 1058
  "An alist representing WoMan buffers that are already decoded.
Each element is of the form (FILE-NAME . BUFFER-NAME).")
Eli Zaretskii's avatar
Eli Zaretskii committed
1059 1060 1061 1062 1063

(defvar woman-buffer-number 0
  "Ordinal number of current buffer entry in `woman-buffer-alist'.
The ordinal numbers start from 0.")

1064 1065 1066 1067 1068
(defvar woman-if-conditions-true '(?n ?e ?o)
  "List of one-character built-in condition names that are true.
Should include ?e, ?o (page even/odd) and either ?n (nroff) or ?t (troff).
Default is '(?n ?e ?o).  Set via `woman-emulation'.")

Eli Zaretskii's avatar
Eli Zaretskii committed
1069 1070 1071 1072 1073 1074

;;; Specialized utility functions:

;;; Fast deletion without saving on the kill ring (cf. simple.el):

(defun woman-delete-line (&optional arg)
1075
  "Delete rest of current line; if all blank then delete thru newline.
Eli Zaretskii's avatar
Eli Zaretskii committed
1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121
With a numeric argument ARG, delete that many lines from point.
Negative arguments delete lines backward."
  ;; This is a non-interactive version of kill-line in simple.el that
  ;; deletes instead of killing and assumes kill-whole-line is nil,
  ;; which is essential!
  (delete-region (point)
		 (progn
		   (if arg
		       (forward-line arg)
		     (if (eobp)
			 (signal 'end-of-buffer nil))
		     (if (looking-at "[ \t]*$")
			 (forward-line 1)
		       (end-of-line)))
		 (point))))

(defsubst woman-delete-whole-line ()
  "Delete current line from beginning including eol."
  (beginning-of-line)
  (woman-delete-line 1))

(defsubst woman-delete-following-space ()
  "Delete all spaces and tabs FOLLOWING point (cf. `delete-horizontal-space')."
  ;; cf. delete-horizontal-space in simple.el:
  (delete-region (point) (progn (skip-chars-forward " \t") (point))))

(defsubst woman-delete-match (subexp)
  "Delete subexpression SUBEXP of buffer text matched by last search."
  (delete-region (match-beginning subexp) (match-end subexp)))

;; delete-char does not kill by default
;; delete-backward-char does not kill by default
;; delete-horizontal-space does not kill
;; delete-blank-lines does not kill


;;; File handling:

(defvar woman-expanded-directory-path nil
  "Expanded directory list cache.  Resetting to nil forces update.")

(defvar woman-topic-all-completions nil
  "Expanded topic alist cache.  Resetting to nil forces update.")

;;;###autoload
(defun woman (&optional topic re-cache)
1122
  "Browse UN*X man page for TOPIC (Without using external Man program).
Eli Zaretskii's avatar
Eli Zaretskii committed
1123 1124 1125 1126 1127 1128 1129
The major browsing mode used is essentially the standard Man mode.
Choose the filename for the man page using completion, based on the
topic selected from the directories specified in `woman-manpath' and
`woman-path'.  The directory expansions and topics are cached for
speed, but a non-nil interactive argument forces the caches to be
updated (e.g. to re-interpret the current directory).

1130 1131
Used non-interactively, arguments are optional: if given then TOPIC
should be a topic string and non-nil RE-CACHE forces re-caching."
Eli Zaretskii's avatar
Eli Zaretskii committed
1132 1133
  (interactive (list nil current-prefix-arg))
  ;; The following test is for non-interactive calls via gnudoit etc.
1134
  (if (or (not (stringp topic)) (string-match "\\S " topic))
Eli Zaretskii's avatar
Eli Zaretskii committed
1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145
      (let ((file-name (woman-file-name topic re-cache)))
	(if file-name
	    (woman-find-file file-name)
	  (message
	   "WoMan Error: No matching manual files found in search path")
	  (ding))
	)
    (message "WoMan Error: No topic specified in non-interactive call")
    (ding))
  )

1146
;; Allow WoMan to be called via the standard Help menu:
Eli Zaretskii's avatar
Eli Zaretskii committed
1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164
(define-key-after menu-bar-manuals-menu [woman]
  '(menu-item "Read Man Page (WoMan)..." woman
	:help "Man-page documentation Without Man") t)

(defvar woman-cached-data nil
  "A list of cached data used to determine cache validity.
Set from the cache by `woman-read-directory-cache'.")

(defun woman-cached-data ()
  "Generate a list of data used to determine cache validity.
Called both to generate and to check the cache!"
  ;; Must use substituted paths because values of env vars may change!
  (list woman-cache-level
	(mapcar 'substitute-in-file-name woman-manpath)
	(mapcar 'substitute-in-file-name woman-path)))

(defun woman-read-directory-cache ()
  "Load the directory and topic cache.
1165 1166
It is loaded from the file named by the variable `woman-cache-filename'.
Return t if the file exists, nil otherwise."
Eli Zaretskii's avatar
Eli Zaretskii committed
1167 1168 1169 1170 1171 1172 1173
  (and
   woman-cache-filename
   (load woman-cache-filename t nil t)	; file exists
   (equal woman-cached-data (woman-cached-data)))) ; cache valid

(defun woman-write-directory-cache ()
  "Save the directory and topic cache.
1174
It is saved to the file named by the variable `woman-cache-filename'."
Eli Zaretskii's avatar
Eli Zaretskii committed
1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202
  (if woman-cache-filename
      (save-excursion			; to restore current buffer
	;; Make a temporary buffer; name starting with space "hides" it.
	(let ((standard-output
	       (set-buffer (generate-new-buffer "WoMan tmp buffer")))
	      (backup-inhibited t))
	  ;; (switch-to-buffer standard-output t) ; only for debugging
	  (buffer-disable-undo standard-output)
	  (princ
	   ";;; WoMan directory and topic cache -- generated automatically\n")
	  (print
	   ;; For data validity check:
	   `(setq woman-cached-data ',(woman-cached-data)))
	  (print
	   `(setq woman-expanded-directory-path
		  ',woman-expanded-directory-path))
	  (print
	   `(setq woman-topic-all-completions
		  ',woman-topic-all-completions))
	  (write-file woman-cache-filename) ; write CURRENT buffer
	  (kill-buffer standard-output)
	  ))))

(defvar woman-topic-history nil "Topic read history.")
(defvar woman-file-history nil "File-name read history.")

(defun woman-file-name (topic &optional re-cache)
  "Get the name of the UN*X man-page file describing a chosen TOPIC.
1203 1204 1205 1206
When `woman' is called interactively, the word at point may be used as
the topic or initial topic suggestion, subject to the value of the
user option `woman-topic-at-point'.  Return nil if no file can be found.
Optional argument RE-CACHE, if non-nil, forces the cache to be re-read."
Eli Zaretskii's avatar
Eli Zaretskii committed
1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223
  ;; Handle the caching of the directory and topic lists:
  (if (and (not re-cache)
	   (or
	    (and woman-expanded-directory-path woman-topic-all-completions)
	    (woman-read-directory-cache)))
      ()
    (message "Building list of manual directory expansions...")
    (setq woman-expanded-directory-path
	  (woman-expand-directory-path woman-manpath woman-path))
    (message "Building completion list of all manual topics...")
    (setq woman-topic-all-completions
	  (woman-topic-all-completions woman-expanded-directory-path))
    (woman-write-directory-cache))
  ;; There is a problem in that I want to offer case-insensitive
  ;; completions, but to return only a case-sensitive match.  This
  ;; does not seem to work properly by default, so I re-do the
  ;; completion if necessary.
1224 1225
  (let (files
	(default (current-word)))
Eli Zaretskii's avatar
Eli Zaretskii committed
1226 1227 1228 1229 1230 1231 1232
    (or (stringp topic)
	(and (eq t
		 (if (boundp 'woman-topic-at-point)
		     woman-topic-at-point
		   ;; Was let-bound when file loaded, so ...
		   (setq woman-topic-at-point woman-topic-at-point-default)))
	     (setq topic
1233
		   (or (current-word t) ""))	; only within or adjacent to word
Eli Zaretskii's avatar
Eli Zaretskii committed
1234 1235 1236
	     (assoc topic woman-topic-all-completions))
	(setq topic
	      (completing-read
1237 1238 1239
	       (if default
		   (format "Manual entry (default `%s'): " default)
		 "Manual entry: ")
Eli Zaretskii's avatar
Eli Zaretskii committed
1240
	       woman-topic-all-completions nil 1
1241 1242 1243
	       nil
	       'woman-topic-history
	       ;; Default topic.
Eli Zaretskii's avatar
Eli Zaretskii committed
1244
	       (and woman-topic-at-point
1245
		    default))))
Eli Zaretskii's avatar
Eli Zaretskii committed
1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295
    ;; Note that completing-read always returns a string.
    (if (= (length topic) 0)
	nil				; no topic, so no file!
      (cond
       ((setq files (woman-file-name-all-completions topic)))
       ;; Complete topic more carefully, i.e. use the completion
       ;; rather than the string entered by the user:
       ((setq files (all-completions topic woman-topic-all-completions))
	(while (/= (length topic) (length (car files)))
	  (setq files (cdr files)))
	(setq files (woman-file-name-all-completions (car files)))))
      (cond
       ((null files) nil)		; no file found for topic.
       ((null (cdr files)) (car (car files))) ; only 1 file for topic.
       (t
	;; Multiple files for topic, so must select 1.
	;; Unread the command event (TAB = ?\t = 9) that runs the command
	;; `minibuffer-complete' in order to automatically complete the
	;; minibuffer contents as far as possible.
	(setq unread-command-events '(9))	; and delete any type-ahead!
	(completing-read "Manual file: " files nil 1
			 (try-completion "" files) 'woman-file-history)))
      )))

(defun woman-select (predicate list)
  "Select unique elements for which PREDICATE is true in LIST.
\(Note that this function changes the value of LIST.)"
  ;; Intended to be fast by avoiding recursion and list copying.
  (while (and list
	      (or
	       (member (car list) (cdr list))
	       (not (funcall predicate (car list)))))
    (setq list (cdr list)))
  (if list
      (let ((newlist list) cdr_list)
	(while (setq cdr_list (cdr list))
	  (if (and
	       (not (member (car cdr_list) (cdr cdr_list)))
	       (funcall predicate (car cdr_list)))
	      (setq list cdr_list)
	    (setcdr list (cdr cdr_list)))
	  )
	newlist)))

(defun woman-file-readable-p (dir)
  "Return t if DIR is readable, otherwise log a warning."
  (or (file-readable-p dir)
      (WoMan-warn "Ignoring unreadable `manpath' directory tree `%s'!" dir)))

(defun woman-directory-files (head dir)
1296
  "Return a sorted list of files in directory HEAD matching regexp in DIR.
Eli Zaretskii's avatar
Eli Zaretskii committed
1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311
Value is a sorted list of the absolute pathnames of all the files in
directory HEAD, or the current directory if HEAD is nil, that match the
regexp that is the final component of DIR.  Log a warning if list is empty."
  (or (directory-files
       (or head (directory-file-name default-directory)) ; was "."
       t
       (file-name-nondirectory dir))
      (WoMan-warn "No directories match `woman-path' entry `%s'!" dir)))

(defun woman-file-accessible-directory-p (dir)
  "Return t if DIR is accessible, otherwise log a warning."
  (or (file-accessible-directory-p dir)
      (WoMan-warn "Ignoring inaccessible `man-page' directory `%s'!" dir)))

(defun woman-expand-directory-path (woman-manpath woman-path)
1312 1313 1314
  "Expand the manual directories in WOMAN-MANPATH and WOMAN-PATH.
WOMAN-MANPATH should be a list of general manual directories, while
WOMAN-PATH should be a list of specific manual directory regexps.
Eli Zaretskii's avatar
Eli Zaretskii committed
1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346
Ignore any paths that are unreadable or not directories."
  ;; Allow each path to be a single string or a list of strings:
  (if (not (listp woman-manpath)) (setq woman-manpath (list woman-manpath)))
  (if (not (listp woman-path)) (setq woman-path (list woman-path)))
  (let (dir head dirs)
    (while woman-manpath
      (setq dir (car woman-manpath)
	    woman-manpath (cdr woman-manpath))
      (if (and dir (woman-file-readable-p dir))
	  ;; NB: `parse-colon-path' creates null elements for
	  ;; redundant (semi-)colons and trailing `/'s!
	  ;; If does not actually matter here if dir ends with `/'.
	  ;; Need regexp "man" here to avoid "cat?", `.', `..', etc.
	  (setq dir (woman-canonicalize-dir dir)
		dirs (nconc dirs (directory-files
				  dir t woman-manpath-man-regexp)))))
    (while woman-path
      (setq dir (car woman-path)
	    woman-path (cdr woman-path))
      (if (or (null dir)
	      (null (setq dir (woman-canonicalize-dir dir)
			  head (file-name-directory dir)))
	      (woman-file-readable-p head))
	  (setq dirs
		(if dir
		    (nconc dirs (woman-directory-files head dir))
		  (cons (directory-file-name default-directory) dirs))
		;; was "." -- at head of list for later filtering
		)))
    (woman-select 'woman-file-accessible-directory-p dirs)))

(defun woman-canonicalize-dir (dir)
1347
  "Canonicalize the directory name DIR.
Eli Zaretskii's avatar
Eli Zaretskii committed
1348 1349 1350 1351 1352 1353
Any UN*X-style environment variables are evaluated first."
  (setq dir (expand-file-name (substitute-in-file-name dir)))
  ;; A path that ends with / matches all directories in it,
  ;; including `.' and `..', so remove any trailing / !!!
  (if (string= (substring dir -1) "/")
      (setq dir (substring dir 0 -1)))
Juanma Barranquero's avatar
Juanma Barranquero committed
1354
  (if (memq system-type '(windows-nt ms-dos cygwin)) ; what else?
Eli Zaretskii's avatar
Eli Zaretskii committed
1355 1356 1357 1358 1359 1360
      ;; Match capitalization used by `file-name-directory':
      (setq dir (concat (file-name-directory dir)
			(file-name-nondirectory dir))))
  dir)

(defsubst woman-not-member (dir path)
1361
  "Return t if DIR is not a member of the list PATH, nil otherwise.
Eli Zaretskii's avatar
Eli Zaretskii committed
1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373
If DIR is `.' it is first replaced by the current directory."
  (not (member dir path)))

(defun woman-topic-all-completions (path)
  "Return an alist of the man files in all man directories in the list PATH.
The cdr of each alist element is the path-index / filename."
  ;; Support 3 levels of caching: each element of the alist `files'
  ;; will be a list of the first `woman-cache-level' elements of the
  ;; following list: (topic path-index filename).  This alist `files'
  ;; is re-processed by `woman-topic-all-completions-merge'.
  (let (dir files (path-index 0))	; indexing starts at zero
    (while path
1374
      (setq dir (pop path))
Eli Zaretskii's avatar
Eli Zaretskii committed
1375
      (if (woman-not-member dir path)	; use each directory only once!
1376 1377
	  (push (woman-topic-all-completions-1 dir path-index)
		files))
Eli Zaretskii's avatar
Eli Zaretskii committed
1378 1379
      (setq path-index (1+ path-index)))
    ;; Uniquefy topics:
1380 1381 1382 1383
    ;; Concate all lists with a single nconc call to
    ;; avoid retraversing the first lists repeatedly  -- dak
    (woman-topic-all-completions-merge
     (apply #'nconc files))))
Eli Zaretskii's avatar
Eli Zaretskii committed
1384 1385

(defun woman-topic-all-completions-1 (dir path-index)
1386 1387 1388 1389 1390 1391 1392 1393 1394 1395
  "Return an alist of the man topics in directory DIR with index PATH-INDEX.
A topic is a filename sans type-related extensions.
Support 3 levels of caching: each element of the alist will be a list
of the first `woman-cache-level' elements from the following list:
\(topic path-index filename)."
  ;; This function used to check that each file in the directory was
  ;; not itself a directory, but this is very slow and should be
  ;; unnecessary.  So let us assume that `woman-file-regexp' will
  ;; filter out any directories, which probably should not be there
  ;; anyway, i.e. it is a user error!
1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417
  ;;
  ;; Don't sort files: we do that when merging, anyway.  -- dak
  (let (newlst (lst (directory-files dir nil woman-file-regexp t))
	       ;; Make an explicit regexp for stripping extension and
	       ;; compression extension: file-name-sans-extension is a
	       ;; far too costly function.  -- dak
	       (ext (format "\\(\\.[^.\\/]*\\)?\\(%s\\)?\\'"
			    woman-file-compression-regexp)))
    ;; Use a loop instead of mapcar in order to avoid the speed
    ;; penalty of binding function arguments.  -- dak
      (dolist (file lst newlst)
	(push
	 (cons
	  (if (string-match ext file)
	      (substring file 0 (match-beginning 0))
	    file)
	  (and (> woman-cache-level 1)
	       (cons
		path-index
		(and (> woman-cache-level 2)
		     (list file)))))
	 newlst))))
Eli Zaretskii's avatar
Eli Zaretskii committed
1418 1419 1420

(defun woman-topic-all-completions-merge (alist)
  "Merge the alist ALIST so that the keys are unique.
1421
Also make each path-info component into a list.
Eli Zaretskii's avatar
Eli Zaretskii committed
1422
\(Note that this function changes the value of ALIST.)"
1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437
  ;; Replaces unreadably "optimized" O(n^2) implementation.
  ;; Instead we use sorting to merge stuff efficiently.  -- dak
  (let (elt newalist)
    ;; Sort list into reverse order
    (setq alist (sort alist (lambda(x y) (string< (car y) (car x)))))
    ;; merge duplicate keys.
    (if (> woman-cache-level 1)
	(while alist
	  (setq elt (pop alist))
	  (if (equal (car elt) (caar newalist))
	      (unless (member (cdr elt) (cdar newalist))
		(setcdr (car newalist) (cons (cdr elt)
					     (cdar newalist))))
	    (setcdr elt (list (cdr elt)))
	    (push elt newalist)))
Eli Zaretskii's avatar
Eli Zaretskii committed
1438
    ;; woman-cache-level = 1 => elements are single-element lists ...
1439 1440 1441 1442 1443
      (while alist
	(setq elt (pop alist))
	(unless (equal (car elt) (caar newalist))
	  (push elt newalist))))
    newalist))
Eli Zaretskii's avatar
Eli Zaretskii committed
1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486

(defun woman-file-name-all-completions (topic)
  "Return an alist of the files in all man directories that match TOPIC."
  ;; Support 3 levels of caching: each element of
  ;; woman-topic-all-completions is a list of one of the forms:
  ;;   (topic)
  ;;   (topic (path-index) (path-index) ... )
  ;;   (topic (path-index filename) (path-index filename) ... )
  ;; where the are no duplicates in the value lists.
  ;; Topic must match first `word' of filename, so ...
  (let ((topic-regexp
	 (concat
	  "\\`" (regexp-quote topic)	; first `word'
	  "\\(\\..+\\)*"		; optional subsequent `words'
	  woman-file-regexp))		; extension
	(topics woman-topic-all-completions)
	(path woman-expanded-directory-path)
	dir files)
    (if (cdr (car topics))
	;; Use cached path-info to locate files for each topic:
	(let ((path-info (cdr (assoc topic topics)))
	      filename)
	  (while path-info
	    (setq dir (nth (car (car path-info)) path)
		  filename (car (cdr (car path-info)))
		  path-info (cdr path-info)
		  files (nconc files
			       ;; Find the actual file name:
			       (if filename
				   (list (concat dir "/" filename))
				 (directory-files dir t topic-regexp)
				 )))))
      ;; Search path for the files for each topic:
      (while path
	(setq dir (car path)
	      path (cdr path))
	(if (woman-not-member dir path)	; use each directory only once!
	    (setq files (nconc files
			       (directory-files dir t topic-regexp))))
	))
    (mapcar 'list files)
    ))

1487

Eli Zaretskii's avatar
Eli Zaretskii committed
1488 1489 1490 1491 1492 1493 1494 1495
;;; dired support

(defun woman-dired-define-key (key)
  "Bind the argument KEY to the command `woman-dired-find-file'."
  (define-key dired-mode-map key 'woman-dired-find-file))

(defsubst woman-dired-define-key-maybe (key)
  "If KEY is undefined in Dired, bind it to command `woman-dired-find-file'."
1496 1497
  (if (or (eq (lookup-key dired-mode