Finalize support for restricted mode option.
[clon.git] / src / output / sheet.lisp
blob87f7b0756be5e395781fadd150c08b21061a609f
1 ;;; sheet.lisp --- Sheet handling
3 ;; Copyright (C) 2010, 2011 Didier Verna
5 ;; Author: Didier Verna <didier@lrde.epita.fr>
6 ;; Maintainer: Didier Verna <didier@lrde.epita.fr>
8 ;; This file is part of Clon.
10 ;; Permission to use, copy, modify, and distribute this software for any
11 ;; purpose with or without fee is hereby granted, provided that the above
12 ;; copyright notice and this permission notice appear in all copies.
14 ;; THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
15 ;; WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
16 ;; MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
17 ;; ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
18 ;; WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
19 ;; ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
20 ;; OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
23 ;;; Commentary:
25 ;; Contents management by FCM version 0.1.
28 ;;; Code:
30 (in-package :com.dvlsoft.clon)
31 (in-readtable :com.dvlsoft.clon)
34 ;; ==========================================================================
35 ;; The Sheet Class
36 ;; ==========================================================================
38 (defclass sheet ()
39 ((output-stream :documentation "The sheet's output stream."
40 :type stream
41 :reader output-stream
42 :initarg :output-stream)
43 (line-width :documentation "The sheet's line width."
44 :type (integer 1)
45 :reader line-width
46 :initarg :line-width)
47 (highlightp :documentation "Whether to highlight SHEET's output."
48 :initarg :highlightp
49 :reader highlightp)
50 (sface-tree :documentation "The sheet's sface tree."
51 :reader sface-tree)
52 (column :documentation "The sheet's current column."
53 :type (integer 0)
54 :accessor column
55 :initform 0)
56 (frames :documentation "The stack of currently open frames."
57 :type list
58 :accessor frames
59 :initform nil))
60 (:documentation "The SHEET class.
61 This class implements the notion of sheet for printing Clon help."))
64 ;; ------------
65 ;; Frame access
66 ;; ------------
68 (defun push-frame (sheet frame)
69 "Push a new frame to SHEET's frames."
70 (push frame (frames sheet)))
72 (defun pop-frame (sheet)
73 "Pop SHEET's current frame."
74 (pop (frames sheet)))
76 (defun current-frame (sheet)
77 "Return SHEET's current frame."
78 (car (frames sheet)))
80 (defmacro map-frames (function (sheet &key reverse))
81 "Map FUNCTION over SHEET's frames.
82 If REVERSE, map in reverse order."
83 `(mapc ,function
84 ,(if reverse
85 `(nreverse (copy-list (frames ,sheet)))
86 `(frames ,sheet))))
90 ;; ==========================================================================
91 ;; Sheet Processing
92 ;; ==========================================================================
94 ;; -------------------------
95 ;; ISO/IEC 6429 SGR handling
96 ;; -------------------------
98 (defmacro highlight-property-ecase (property value &body clauses)
99 "Create an ECASE form to extract PROPERTY's VALUE escape sequence.
100 Each clause looks like: (PROPERTY-NAME (VALUE-OR-VALUE-LIST ESCAPE-SEQUENCE)*).
101 The value-matching part will itself be enclosed in an ECASE expression.
102 In addition, the special clause syntax (BOOLEAN <PROPERTY-NAME> <YES> <NO>)
103 is a shortcut for: (PROPERTY-NAME ((on t) YES) ((off nil) NO))."
104 `(ecase ,property
105 ,@(mapcar (lambda (clause)
106 (if (eq (car clause) 'boolean)
107 `(,(cadr clause)
108 (ecase ,value
109 ((on t) ,(caddr clause))
110 ((off nil) ,(cadddr clause))))
111 `(,(car clause)
112 (ecase ,value
113 ,@(cdr clause)))))
114 clauses)))
116 (defun highlight-property-instance-escape-sequence (instance)
117 "Return highlight property INSTANCE's escape sequence."
118 (highlight-property-ecase
119 (highlight-property-instance-name instance)
120 (highlight-property-instance-value instance)
121 ;; FAINT is not well supported
122 (intensity (bold 1) (faint 2) ((normal nil) 22))
123 (boolean italicp 3 23)
124 ;; DOUBLE is not well supported
125 (underline ((single on t) 4) (double 21) ((none off nil) 24))
126 ;; RAPID is not well supported
127 (blink ((slow on t) 5) (rapid 6) ((off nil) 25))
128 (boolean inversep 7 27)
129 (boolean concealedp 8 28)
130 ;; I've seen the following two properties in some code, but I'm not sure
131 ;; I've seen them work anywhere.
132 (boolean crossed-out-p 9 29)
133 (boolean framedp 51 54)
134 (foreground (black 30) (red 31) (green 32) (yellow 33) (blue 34)
135 (magenta 35) (cyan 36) (white 37) ((reset nil) 39))
136 (background (black 40) (red 41) (green 42) (yellow 43) (blue 44)
137 (magenta 45) (cyan 46) (white 47) ((reset nil) 49))))
139 (defun princ-highlight-property-instances (sheet instances)
140 "Princ highlight proeprty INSTANCES on SHEET's stream."
141 (when instances
142 (format (output-stream sheet) "~C[~A~{;~A~}m"
143 ;; #### NOTE: #\escape is not EBCDIC (it's ANSI), and CLHS 13.1.7
144 ;; doesn't mention ANSI (thanks Pascal Bourguignon for spotting this),
145 ;; so technically, #\escape is not a standard Common Lisp character. All
146 ;; the lisp implementations supported by Clon seem to understand it
147 ;; however (previously, I used #\esc but ABCL doesn't like it).
148 #\escape
149 (highlight-property-instance-escape-sequence (car instances))
150 (mapcar #'highlight-property-instance-escape-sequence
151 (cdr instances)))))
154 ;; ----------------
155 ;; Low level output
156 ;; ----------------
158 (defun princ-char (sheet char)
159 "Princ CHAR on SHEET's stream and increment the column position.
160 The effect of printing CHAR must be exactly to move right by one column, so
161 control characters, as well as newlines and tabs are forbidden here."
162 ;; #### FIXME: control chars not handled.
163 (assert (not (member char '(#\newline #\tab))))
164 (princ char (output-stream sheet))
165 (incf (column sheet)))
167 (defun princ-string (sheet string)
168 "Princ STRING on SHEET's stream and update the column position.
169 The effect of printing STRING must be exactly to move right by the
170 corresponding string length, so control characters, as well as newlines and
171 tabs are forbidden here."
172 ;; #### FIXME: control chars not handled.
173 (assert (notany (lambda (char) (member char '(#\newline #\tab))) string))
174 (princ string (output-stream sheet))
175 (incf (column sheet) (length string)))
177 (defun princ-spaces (sheet number)
178 "Princ NUMBER spaces to SHEET's stream and update the column position."
179 (princ-string sheet (make-string number :initial-element #\space)))
181 ;; #### NOTE: the current column might in fact be already past the desired
182 ;; one. For instance, since we don't do hyphenation, something too big to fit
183 ;; in the current frame will overfull it.
184 (defun reach-column (sheet column)
185 "Reach COLUMN on SHEET by princ'ing spaces."
186 (when (< (column sheet) column)
187 (princ-spaces sheet (- column (column sheet)))))
190 ;; --------------
191 ;; Logical output
192 ;; --------------
194 (defclass sface (face)
195 ((sibling :documentation "The SFace's raw sibling."
196 :reader sibling))
197 (:documentation "The SFACE class.
198 An SFace is the association of a face and its raw sibling. The sibling is used
199 to create subfaces which would be missing from the original, user defined one."))
201 (defun make-raw-sface (sibling &aux (sface (copy-instance sibling 'sface)))
202 "Return a new SFace based on SIBLING.
203 This function does not consider SIBLING as a face tree:
204 only face properties are copied; the face parent and children are set to nil."
205 (setf (slot-value sface 'parent) nil)
206 (setf (slot-value sface 'subfaces) nil)
207 (setf (slot-value sface 'sibling) sibling)
208 sface)
210 (defstruct frame
211 "The FRAME structure.
212 This structure hold layout properties used for printing."
213 sface
214 left-margin
215 right-margin)
217 (defstruct highlight-property-instance
218 "The HIGHLIGHT-PROEPRTY-INSTANCE structure."
219 name
220 value)
222 (defstruct (highlight-frame (:include frame))
223 "The HIGHLIGHT-FRAME structure.
224 This structure holds both layout and highlight properties used for printing."
225 highlight-property-instances)
227 ;; Shortcut accessors to the top frame:
228 (defun current-sface (sheet)
229 "Return SHEET's current sface or nil."
230 (when (frames sheet)
231 (frame-sface (current-frame sheet))))
233 (defun current-left-margin (sheet)
234 "Return SHEET's current left margin."
235 (if (frames sheet)
236 (frame-left-margin (current-frame sheet))
239 (defun current-right-margin (sheet)
240 "Return SHEET's current right margin."
241 (if (frames sheet)
242 (frame-right-margin (current-frame sheet))
243 (line-width sheet)))
245 (defun available-right-margin (sheet)
246 "Return SHEET's available right margin.
247 This margin is the first non-self margin specified by a frame. All inner self
248 frames can potentially write until the available right margin."
249 (map-frames (lambda (frame)
250 (let ((right-margin (frame-right-margin frame)))
251 (when (numberp right-margin)
252 (return-from available-right-margin right-margin))))
253 (sheet))
254 (line-width sheet))
256 (defgeneric open-frame (sheet frame)
257 (:documentation "Open FRAME on SHEET.")
258 (:method-combination progn :most-specific-last)
259 (:method progn (sheet (frame frame))
260 "Reach the frame's left margin."
261 (reach-column sheet (frame-left-margin frame)))
262 (:method progn (sheet (frame highlight-frame))
263 "Reach the frame's left margin and output its highlight properties."
264 (princ-highlight-property-instances
265 sheet (highlight-frame-highlight-property-instances frame))))
267 (defgeneric close-frame (sheet frame)
268 (:documentation "Close FRAME on SHEET.")
269 (:method-combination progn :most-specific-last)
270 (:method progn (sheet (frame frame)
271 &aux (right-margin (frame-right-margin frame)))
272 "Reach FRAME's right margin if it has one."
273 (when (numberp right-margin)
274 (reach-column sheet right-margin)))
275 (:method progn (sheet (frame highlight-frame))
276 "Restore the upper frame's highlight properties."
277 (princ-highlight-property-instances
278 sheet
279 (mapcar (lambda (instance)
280 (make-highlight-property-instance
281 :name (highlight-property-instance-name instance)
282 :value (when (parent (frame-sface frame))
283 (face-highlight-property-value
284 (parent (frame-sface frame))
285 (highlight-property-instance-name instance)))))
286 (highlight-frame-highlight-property-instances frame)))))
288 (defun close-line (sheet)
289 "Close all frames on SHEET's current line and go to next line."
290 (map-frames (lambda (frame)
291 (close-frame sheet frame))
292 (sheet))
293 (terpri (output-stream sheet))
294 (setf (column sheet) 0))
296 (defun open-line (sheet)
297 "Open all frames on SHEET's current line."
298 (assert (zerop (column sheet)))
299 (map-frames (lambda (frame)
300 (open-frame sheet frame))
301 (sheet :reverse t)))
303 (defun open-next-line (sheet)
304 "Close SHEET's current line and open the next one."
305 (close-line sheet)
306 (open-line sheet))
308 ;; #### FIXME: control chars not handled.
309 (defun print-string (sheet string)
310 "Output STRING to SHEET.
311 STRING is output within the current frame's bounds.
312 Spacing characters are honored but newlines might replace spaces when the
313 output reaches the rightmost bound."
314 (assert (and string (not (zerop (length string)))))
315 ;; #### FIXME: I don't remember, but this might not work: don't I need to
316 ;; honor the frames'sfaces here instead of blindly spacing ?? Or am I sure
317 ;; I'm in the proper frame/sface ?
318 ;; First, adjust the tabbing.
319 (loop :with len := (length string) :and i := 0
320 :while (< i len)
321 :do (case (aref string i)
322 (#\space
323 (if (>= (column sheet) (available-right-margin sheet))
324 ;; If we're at the end of the line, turn the space into a
325 ;; newline.
326 (open-next-line sheet)
327 ;; Otherwise, just output it.
328 (princ-char sheet #\space))
329 (incf i))
330 (#\tab
331 ;; Here, we get the real number of spaces to insert in order to
332 ;; reach the next tab position with respect to the current
333 ;; frame. #### FIXME: get a real tabsize
334 (let ((spaces (+ (- (* (ceiling (/ (- (column sheet)
335 (current-left-margin
336 sheet))
339 (column sheet))
340 (current-left-margin sheet))))
341 (cond ((< (+ (column sheet) spaces)
342 (available-right-margin sheet))
343 (princ-spaces sheet spaces))
345 ;; If the requested tab position is too far away, we
346 ;; simply go next line. There's not much that we can
347 ;; do to repair the layout anyway.
348 (open-next-line sheet))))
349 (incf i))
350 (#\newline
351 (open-next-line sheet)
352 (incf i))
353 (otherwise
354 (let* ((end (or (position-if
355 (lambda (char)
356 (member char '(#\space #\tab #\newline)))
357 string
358 :start i)
359 len))
360 (chunk-width (- end i))
361 (available-width (- (available-right-margin sheet)
362 (column sheet)))
363 (full-width (- (available-right-margin sheet)
364 (current-left-margin sheet))))
365 (cond ((<= chunk-width available-width)
366 ;; The chunk fits right here, so go for it.
367 (princ-string sheet (subseq string i end))
368 (setq i end))
369 ((<= chunk-width full-width)
370 ;; The chunk fits if we put it on the next line, so
371 ;; open the next line. Note that we don't actually
372 ;; output the word right now. This will be handled by
373 ;; the next LOOP iteration.
374 (open-next-line sheet))
375 ;; The chunk wouldn't even fit on a line of its own, so
376 ;; we have no other choice than splitting it at a
377 ;; non-space position. When we do, we also insert an
378 ;; hyphenation mark at the end of the chunk. We know
379 ;; from open-sface that every frame has is least two
380 ;; characters wide (in other words, we know here that
381 ;; full-width >= 2). However, available-with might
382 ;; already be too small. If that is the case, we must
383 ;; go next-line first.
384 ((< available-width 2)
385 (open-next-line sheet))
387 (setq end (+ i available-width -1))
388 (princ-string sheet (subseq string i end))
389 (princ-char sheet #\-)
390 (setq i end)
391 (open-next-line sheet))))))))
394 ;; ---------------
395 ;; SFace management
396 ;; ---------------
398 (defun find-sface (sface name
399 &aux (sibling (search-face (sibling sface) name :error-me))
400 (sub-sface (search-face sface name)))
401 "Find an sface starting at SFACE named NAME.
402 If the sface can't be found in SFACE's face tree, find one in SFACE's sibling
403 instead, and make a copy of it."
404 (cond (sub-sface
405 ;; #### NOTE: this is a bit dirty. The sibling might already have
406 ;; been set before. It might be better to turn the search procdedure
407 ;; into a generic function, and specialized its behavior.
408 (setf (slot-value sub-sface 'sibling) sibling)
409 sub-sface)
411 ;; #### NOTE: here, we create the missing face *only*. That is, we
412 ;; don't copy a whole raw face tree. Copying the whole raw face tree
413 ;; would perhaps create (hence override) other faces previously
414 ;; defined by the user upper in the face hierarchy, and we want to
415 ;; avoid that.
416 (add-subface sface (make-raw-sface sibling)))))
418 ;; In practice, it could happen that the level of indentation exceeds the
419 ;; line-width (either the theme has something crazy in it, or we just have too
420 ;; many nested levels of indentation) ... We're in trouble here, so let's just
421 ;; stay where we are.
422 (defun safe-left-margin (sheet margin)
423 "Return either MARGIN or a safe value instead.
424 To be safe, margin must be greater than the current left margin and smaller
425 than the currently available margin."
426 (or (when (or (< margin (current-left-margin sheet))
427 (>= margin (available-right-margin sheet)))
428 (current-left-margin sheet))
429 margin))
431 (defun safe-right-margin (sheet left-margin margin)
432 "Return either MARGIN or a safe value instead.
433 To be safe, margin must be greater than LEFT-MARGIN and smaller
434 than the currently available right margin."
435 (or (when (or (<= margin left-margin)
436 (> margin (available-right-margin sheet)))
437 (available-right-margin sheet))
438 margin))
440 (defun open-sface (sheet sface)
441 "Create a frame for SFACE and open it."
442 (assert (visiblep sface))
443 ;; Create the new frame:
444 (let* ((left-margin
445 (safe-left-margin
446 sheet
447 (let ((padding-spec (left-padding sface)))
448 (econd
449 ((eq padding-spec 'self)
450 (column sheet))
451 ((numberp padding-spec)
452 (+ (current-left-margin sheet) padding-spec))
453 ((listp padding-spec)
454 (destructuring-bind (padding relative-to &optional face-name)
455 padding-spec
456 ;; #### FIXME: should provide better error handling
457 (econd ((and (eq relative-to 'absolute)
458 (null face-name))
459 padding)
460 ((and (eq relative-to :relative-to)
461 (symbolp face-name))
462 (let* ((generation (parent-generation sface
463 face-name))
464 (left-margin
465 (frame-left-margin
466 ;; #### WARNING: we have not open the new
467 ;; frame yet, so decrement the generation
468 ;; level !!
469 (nth (1- generation) (frames sheet)))))
470 (+ left-margin padding))))))))))
471 (right-margin
472 (let ((padding-spec (right-padding sface)))
473 (econd
474 ((eq padding-spec 'self)
475 nil)
476 ((numberp padding-spec)
477 (if (numberp (current-right-margin sheet))
478 (safe-right-margin sheet left-margin
479 (- (current-right-margin sheet)
480 padding-spec))
481 (error "Right padding (face ~A) can't be :relative-to a self right margin (face ~A)."
482 (name sface) (name (current-sface sheet)))))
483 ((listp padding-spec)
484 (destructuring-bind (padding relative-to &optional face-name)
485 padding-spec
486 ;; #### FIXME: should provide better error handling
487 (econd ((and (eq relative-to 'absolute)
488 (null face-name))
489 (safe-right-margin sheet left-margin padding))
490 ((and (eq relative-to :relative-to)
491 (symbolp face-name))
492 (let* ((generation
493 (parent-generation sface face-name))
494 (right-margin
495 (frame-right-margin
496 ;; #### WARNING: we have not
497 ;; open the new frame yet, so
498 ;; decrement the generation
499 ;; level !!
500 (nth (1- generation) (frames sheet)))))
501 (if (numberp right-margin)
502 (safe-right-margin sheet left-margin
503 (- right-margin padding))
504 (error "Can't be :relative-to a self right margin.")))))))))))
505 ;; Despite the "safe" computations above, we still need to check that our
506 ;; new left and right margins let us actually display something.
507 ;; Otherwise, we don't move at all because the layout is too fucked up. A
508 ;; strict minimum is room for 2 characters, so that we can at least
509 ;; display one character and an hyphen. But really, 2 characters wide is
510 ;; already cmpletely insane...
511 (let ((actual-right-margin
512 (or right-margin (available-right-margin sheet))))
513 (unless (>= (- actual-right-margin left-margin) 2)
514 (setq left-margin (current-left-margin sheet)
515 right-margin (current-right-margin sheet))))
516 (push-frame sheet
517 (if (highlightp sheet)
518 (let ((highlight-property-instances
519 (loop :for property :in +highlight-properties+
520 :when (face-highlight-property-set-p
521 sface property)
522 :collect (make-highlight-property-instance
523 :name property
524 :value
525 (face-highlight-property-value
526 sface property)))))
527 (make-highlight-frame :sface sface
528 :left-margin left-margin
529 :right-margin right-margin
530 :highlight-property-instances
531 highlight-property-instances))
532 (make-frame :sface sface
533 :left-margin left-margin
534 :right-margin right-margin))))
535 ;; Open the new frame:
536 (open-frame sheet (current-frame sheet)))
538 (defun close-sface (sheet)
539 "Close SHEET's current sface."
540 (close-frame sheet (current-frame sheet))
541 (pop-frame sheet))
545 ;; =========================================================================
546 ;; The Print Help Protocol
547 ;; =========================================================================
549 (defun help-spec-items-will-print (sface items)
550 "Return t if at least one of ITEMS will print under SFACE."
551 (assert (visiblep sface))
552 (some (lambda (help-spec)
553 (help-spec-will-print sface help-spec))
554 items))
556 (defgeneric help-spec-will-print (sface help-spec)
557 (:documentation "Return t if HELP-SPEC will print under FACE.")
558 (:method :before (sface help-spec)
559 #+(or ccl ecl clisp) (declare (ignore help-spec))
560 (assert (visiblep sface)))
561 (:method (sface help-spec)
562 "Basic help specifications (chars, strings etc) do print."
563 #+(or ccl ecl clisp) (declare (ignore sface help-spec))
565 (:method (sface (help-spec list))
566 "Return t if HELP-SPEC's items will print under HELP-SPEC's face."
567 (let ((subsface (find-sface sface (car help-spec))))
568 (and (visiblep subsface)
569 (help-spec-items-will-print subsface (cdr help-spec))))))
571 (defgeneric get-bottom-padding (sface help-spec)
572 (:documentation "Get HELP-SPEC's bottom-padding under SFACE.")
573 (:method (sface help-spec)
574 #+(or ccl ecl clisp) (declare (ignore sface help-spec))
575 "Basic help specifications (chars, strings etc) don't provide a bottom padding."
576 nil)
577 (:method (sface (help-spec list))
578 "Return the bottom padding of HELP-SPEC's face."
579 (bottom-padding (find-sface sface (car help-spec)))))
581 (defmethod top-padding (other)
582 #+(or ccl ecl clisp) (declare (ignore other))
583 nil)
585 (defmethod top-padding ((help-spec list))
586 (top-padding (car help-spec)))
588 ;; #### NOTE: right now, we get the top-padding of the first item that prints.
589 ;; Note that in case of nested help specifications, the retrieved value is
590 ;; that of the topmost face, and not that of the leaf (which actually prints
591 ;; something). It is not clear to me whether this is good or bad, or whether
592 ;; "it depends". However, I spotted one case in which it gives an unexpected
593 ;; result.
595 ;; Suppose we have two nested groups with header but no contents. The help
596 ;; spec is like this: (group (header "Foo") (contents (group (header
597 ;; "Bar")))). With the refcard theme, the two headers will appear on the same
598 ;; line despite any padding specification. That is because the separation
599 ;; between the first header and the sub-group is a #\space. Since the second
600 ;; group is the first item in the enclosing contents face, its top-padding
601 ;; value is not used.
603 ;; This is bad. I'm not sure what I should do about it. Use the leaf ? Use a
604 ;; max of *all* enclosing faces padding[1] ? Provide padding options specific
605 ;; to what's after/before ? Dammit. This is not worth the trouble...
607 ;; Footnotes: [1] the more I think of it, the more I like this option...
609 (defun get-top-padding (sface items)
610 "Return top padding of the next item in ITEMS that will print under SFACE."
611 (loop :for help-spec :in items
612 :when (help-spec-will-print sface help-spec)
613 :return (when (listp help-spec)
614 (top-padding (find-sface sface (car help-spec))))))
616 ;; #### NOTE: this is where I would like a more expressive dispatch in CLOS.
617 ;; This function should be part of print-help-spec, with two cases:
618 ;; - (face-name items...)
619 ;; - (sface items...)
620 (defun print-faced-help-spec (sheet sface items)
621 "Print all help specification ITEMS on SHEET with SFACE."
622 (when (and (visiblep sface)
623 (help-spec-items-will-print sface items))
624 (open-sface sheet sface)
625 (loop :for help-specs :on items
626 :for help-spec := (car help-specs)
628 (when (help-spec-will-print (current-sface sheet) help-spec)
629 (print-help-spec sheet help-spec)
630 (when (help-spec-items-will-print (current-sface sheet)
631 (cdr help-specs))
632 (let ((vertical-padding
633 (max (or (get-bottom-padding (current-sface sheet)
634 help-spec)
636 (or (get-top-padding (current-sface sheet)
637 (cdr help-specs))
638 -1))))
639 (cond ((>= vertical-padding 0)
640 (print-help-spec sheet
641 (make-string (1+ vertical-padding)
642 :initial-element #\newline)))
643 ((item-separator (current-sface sheet))
644 (print-help-spec sheet (item-separator
645 (current-sface sheet)))))))))
646 (close-sface sheet)))
648 (defgeneric print-help-spec (sheet help-spec)
649 (:documentation "Print HELP-SPEC on SHEET.")
650 (:method :before (sheet help-spec)
651 #+(or ccl ecl clisp) (declare (ignore help-spec))
652 (assert (visiblep (current-sface sheet))))
653 (:method (sheet (char character))
654 "Print CHAR on SHEET with the current face."
655 (print-help-spec sheet (make-string 1 :initial-element char)))
656 ;; ECL and CLISP don't have a SIMPLE-VECTOR class, so we use just VECTOR
657 ;; instead.
658 (:method (sheet (char-vector #+(or ecl clisp) vector
659 #-(or ecl clisp) simple-vector))
660 "Print CHAR-VECTOR on SHEET with the current face."
661 (print-help-spec sheet (coerce char-vector 'string)))
662 (:method (sheet (string string))
663 "Print STRING on SHEET with the current face."
664 (print-string sheet string))
665 (:method (sheet (help-spec list))
666 "Open HELP-SPEC's face and print all of its items with it."
667 (let ((sface (find-sface (current-sface sheet) (car help-spec))))
668 (print-faced-help-spec sheet sface (cdr help-spec)))))
670 (defun print-help (sheet help)
671 "Open the toplevel help face and print HELP on SHEET with it."
672 (let ((items
673 (if (and (listp help) (not (symbolp (car help))))
674 ;; There's already an enclosing list when help for a container
675 ;; is requested directly, or when the complete help is
676 ;; requested, in which case we have the list of synopsis and all
677 ;; synopsis items.
678 help
679 (list help))))
680 (print-faced-help-spec sheet (sface-tree sheet) items)))
684 ;; ==========================================================================
685 ;; Sheet Instance Creation
686 ;; ==========================================================================
688 ;; #### NOTE: I need to bind output-stream here (which is early) because it is
689 ;; required to do the TIOCGWINSZ ioctl business.
690 (defmethod initialize-instance :around
691 ((sheet sheet) &rest keys &key (output-stream *standard-output*)
692 line-width
693 (highlight :auto))
694 "Handle unset line width and AUTO highlight according to OUTPUT-STREAM."
695 ;; In both of the cases below, we must know whether we're printing to a
696 ;; terminal or a simple file.
697 (when (or (not line-width) (eq highlight :auto))
698 (multiple-value-bind (tty-line-width error-message)
699 (when (fboundp 'stream-line-width) ; depends on the termio module
700 (stream-line-width output-stream))
701 (when error-message
702 ;; #### FIXME: a better error printing would be nice.
703 (let (*print-escape*)
704 (format *error-output* "Error: ~A.~%" error-message)))
705 ;; Next, set highlighting.
706 (when (eq highlight :auto)
707 (setq highlight tty-line-width))
708 ;; Finally, set line width.
709 (unless line-width
710 (setq line-width
711 (let ((columns (getenv "COLUMNS")))
712 (if columns
713 (handler-case
714 (coerce (read-from-string columns) '(integer 1))
715 (error (error)
716 ;; #### FIXME: a better error printing would be nice.
717 (let (*print-escape*)
718 (print-object error *error-output*))
719 (or tty-line-width 80)))
720 ;; Yuck. Code duplication.
721 (or tty-line-width 80)))))))
722 (apply #'call-next-method sheet
723 :output-stream output-stream
724 :line-width line-width
725 :highlightp highlight
726 ;; #### NOTE: technically, the call to REMOVE-KEYS below is not
727 ;; needed because in case of duplication, the leftmost initarg is
728 ;; used (see section 7.1.4 "Rules for Initialization Arguments" of
729 ;; the Hyperspec).
730 (remove-keys keys :output-stream :line-width :highlight)))
732 (defun read-sface-tree (pathname)
733 "Read an sface tree from PATHNAME."
734 (make-face-tree
735 (list* 'toplevel
736 (with-open-file (stream pathname)
737 (let ((*package* (find-package :com.dvlsoft.clon)))
738 (loop :for item := (read stream nil stream)
739 :if (eql item stream)
740 :return items
741 :else
742 :collect item :into items))))
743 'sface))
745 (defun try-read-sface-tree (pathname)
746 "Read an sface tree from PATHNAME if it exists or return nil."
747 (when (open pathname :direction :probe)
748 (read-sface-tree pathname)))
750 (defun try-read-theme (pathname)
751 "Read a theme from PATHNAME or PATHNAME.cth if it exists or return nil."
752 ;; #### FIXME: should warn or err if file doesn't exist.
753 (or (try-read-sface-tree pathname)
754 (unless (string= (pathname-type pathname) "cth")
755 (try-read-sface-tree (merge-pathnames pathname
756 (make-pathname :type "cth"))))))
758 ;; #### FIXME: when trying Mac OSX paths, we have a mix of upcase and downcase
759 ;; directory names, for instance, share/clon and Application Support/Clon.
760 ;; Normally, when we append a "themes" subdirectory somewhere, we should
761 ;; respect that. However (at least by default but can this be customized?),
762 ;; the OSX file system is case insensitive. For other file systems, this might
763 ;; break someday.
764 (defmethod initialize-instance :after ((sheet sheet) &key theme search-path)
765 "Finish initialization of SHEET.
766 This involves:
767 - computing SHEET's sface tree from THEME and SEARCH-PATH,
768 - initializing SHEET's toplevel sface's sibling to a raw face tree."
769 (setf (slot-value sheet 'sface-tree)
770 (or (cond ((and theme (or (not search-path)
771 (pathname-directory theme)))
772 (try-read-theme theme))
773 (theme
774 (setq theme
775 (merge-pathnames theme
776 (make-pathname
777 :directory `(:relative "themes"))))
778 (loop :for path :in search-path
779 :for sface-tree := (try-read-theme
780 (merge-pathnames theme path))
781 :until sface-tree
782 :finally (return sface-tree))))
783 (make-raw-face-tree 'sface)))
784 (setf (slot-value (sface-tree sheet) 'sibling) (make-raw-face-tree)))
786 (defun make-sheet
787 (&rest keys &key output-stream search-path theme line-width highlight)
788 "Make a new SHEET."
789 (declare (ignore output-stream search-path theme line-width highlight))
790 (apply #'make-instance 'sheet keys))
792 (defun flush-sheet (sheet)
793 "Flush SHEET."
794 (assert (null (current-sface sheet)))
795 (terpri (output-stream sheet)))
798 ;;; sheet.lisp ends here