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.
25 ;; Contents management by FCM version 0.1.
30 (in-package :com.dvlsoft.clon
)
31 (in-readtable :com.dvlsoft.clon
)
34 ;; ==========================================================================
36 ;; ==========================================================================
39 ((output-stream :documentation
"The sheet's output stream."
42 :initarg
:output-stream
)
43 (line-width :documentation
"The sheet's line width."
47 (highlightp :documentation
"Whether to highlight SHEET's output."
50 (sface-tree :documentation
"The sheet's sface tree."
52 (column :documentation
"The sheet's current column."
56 (frames :documentation
"The stack of currently open frames."
60 (:documentation
"The SHEET class.
61 This class implements the notion of sheet for printing Clon help."))
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."
76 (defun current-frame (sheet)
77 "Return SHEET's current frame."
80 (defmacro map-frames
(function (sheet &key reverse
))
81 "Map FUNCTION over SHEET's frames.
82 If REVERSE, map in reverse order."
85 `(nreverse (copy-list (frames ,sheet
)))
90 ;; ==========================================================================
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))."
105 ,@(mapcar (lambda (clause)
106 (if (eq (car clause
) 'boolean
)
109 ((on t
) ,(caddr clause
))
110 ((off nil
) ,(cadddr clause
))))
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."
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).
149 (highlight-property-instance-escape-sequence (car instances
))
150 (mapcar #'highlight-property-instance-escape-sequence
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
)))))
194 (defclass sface
(face)
195 ((sibling :documentation
"The SFace's raw 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
)
211 "The FRAME structure.
212 This structure hold layout properties used for printing."
217 (defstruct highlight-property-instance
218 "The HIGHLIGHT-PROEPRTY-INSTANCE structure."
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."
231 (frame-sface (current-frame sheet
))))
233 (defun current-left-margin (sheet)
234 "Return SHEET's current left margin."
236 (frame-left-margin (current-frame sheet
))
239 (defun current-right-margin (sheet)
240 "Return SHEET's current right margin."
242 (frame-right-margin (current-frame 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
))))
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
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
))
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
))
303 (defun open-next-line (sheet)
304 "Close SHEET's current line and open the next one."
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
321 :do
(case (aref string i
)
323 (if (>= (column sheet
) (available-right-margin sheet
))
324 ;; If we're at the end of the line, turn the space into a
326 (open-next-line sheet
)
327 ;; Otherwise, just output it.
328 (princ-char sheet
#\space
))
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
)
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
))))
351 (open-next-line sheet
)
354 (let* ((end (or (position-if
356 (member char
'(#\space
#\tab
#\newline
)))
360 (chunk-width (- end i
))
361 (available-width (- (available-right-margin 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
))
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
#\-
)
391 (open-next-line sheet
))))))))
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."
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
)
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
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
))
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
))
440 (defun open-sface (sheet sface
)
441 "Create a frame for SFACE and open it."
442 (assert (visiblep sface
))
443 ;; Create the new frame:
447 (let ((padding-spec (left-padding sface
)))
449 ((eq padding-spec
'self
)
451 ((numberp padding-spec
)
452 (+ (current-left-margin sheet
) padding-spec
))
453 ((listp padding-spec
)
454 (destructuring-bind (padding relative-to
&optional face-name
)
456 ;; #### FIXME: should provide better error handling
457 (econd ((and (eq relative-to
'absolute
)
460 ((and (eq relative-to
:relative-to
)
462 (let* ((generation (parent-generation sface
466 ;; #### WARNING: we have not open the new
467 ;; frame yet, so decrement the generation
469 (nth (1- generation
) (frames sheet
)))))
470 (+ left-margin padding
))))))))))
472 (let ((padding-spec (right-padding sface
)))
474 ((eq padding-spec
'self
)
476 ((numberp padding-spec
)
477 (if (numberp (current-right-margin sheet
))
478 (safe-right-margin sheet left-margin
479 (- (current-right-margin sheet
)
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
)
486 ;; #### FIXME: should provide better error handling
487 (econd ((and (eq relative-to
'absolute
)
489 (safe-right-margin sheet left-margin padding
))
490 ((and (eq relative-to
:relative-to
)
493 (parent-generation sface face-name
))
496 ;; #### WARNING: we have not
497 ;; open the new frame yet, so
498 ;; decrement the generation
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
))))
517 (if (highlightp sheet
)
518 (let ((highlight-property-instances
519 (loop :for property
:in
+highlight-properties
+
520 :when
(face-highlight-property-set-p
522 :collect
(make-highlight-property-instance
525 (face-highlight-property-value
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
))
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
))
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."
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
))
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
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
)
632 (let ((vertical-padding
633 (max (or (get-bottom-padding (current-sface sheet
)
636 (or (get-top-padding (current-sface sheet
)
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
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."
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
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
*)
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
))
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.
711 (let ((columns (getenv "COLUMNS")))
714 (coerce (read-from-string columns
) '(integer 1))
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
730 (remove-keys keys
:output-stream
:line-width
:highlight
)))
732 (defun read-sface-tree (pathname)
733 "Read an sface tree from PATHNAME."
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
)
742 :collect item
:into items
))))
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
764 (defmethod initialize-instance :after
((sheet sheet
) &key theme search-path
)
765 "Finish initialization of SHEET.
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
))
775 (merge-pathnames theme
777 :directory
`(:relative
"themes"))))
778 (loop :for path
:in search-path
779 :for sface-tree
:= (try-read-theme
780 (merge-pathnames theme path
))
782 :finally
(return sface-tree
))))
783 (make-raw-face-tree 'sface
)))
784 (setf (slot-value (sface-tree sheet
) 'sibling
) (make-raw-face-tree)))
787 (&rest keys
&key output-stream search-path theme line-width highlight
)
789 (declare (ignore output-stream search-path theme line-width highlight
))
790 (apply #'make-instance
'sheet keys
))
792 (defun flush-sheet (sheet)
794 (assert (null (current-sface sheet
)))
795 (terpri (output-stream sheet
)))
798 ;;; sheet.lisp ends here