Add tty and listing devices that use VT100 (and xterm) line-drawing
[pspp-builds.git] / src / output / ascii.c
1 /* PSPP - a program for statistical analysis.
2    Copyright (C) 1997-9, 2000, 2007 Free Software Foundation, Inc.
3
4    This program is free software: you can redistribute it and/or modify
5    it under the terms of the GNU General Public License as published by
6    the Free Software Foundation, either version 3 of the License, or
7    (at your option) any later version.
8
9    This program is distributed in the hope that it will be useful,
10    but WITHOUT ANY WARRANTY; without even the implied warranty of
11    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12    GNU General Public License for more details.
13
14    You should have received a copy of the GNU General Public License
15    along with this program.  If not, see <http://www.gnu.org/licenses/>. */
16
17 #include <config.h>
18
19 #include <ctype.h>
20 #include <errno.h>
21 #include <limits.h>
22 #include <stdlib.h>
23
24 #include <data/file-name.h>
25 #include <libpspp/alloc.h>
26 #include <libpspp/assertion.h>
27 #include <libpspp/compiler.h>
28 #include <libpspp/pool.h>
29 #include <libpspp/start-date.h>
30 #include <libpspp/version.h>
31
32 #include "chart.h"
33 #include "error.h"
34 #include "minmax.h"
35 #include "output.h"
36
37 #include "gettext.h"
38 #define _(msgid) gettext (msgid)
39
40 /* ASCII driver options: (defaults listed first)
41
42    output-file="pspp.list"
43    paginate=on|off              Formfeeds are desired?
44    tab-width=8                  Width of a tab; 0 to not use tabs.
45
46    headers=on|off               Put headers at top of page?
47    emphasis=bold|underline|none Style to use for emphasis.
48    length=66
49    width=130
50    squeeze=off|on               Squeeze multiple newlines into exactly one.
51
52    top-margin=2
53    bottom-margin=2
54
55    box[x]="strng"               Sets box character X (X in base 4: 0-3333).
56    init="string"                Set initialization string.
57  */
58
59 /* Disable messages by failed range checks. */
60 /*#define SUPPRESS_WARNINGS 1 */
61
62 /* Line styles bit shifts. */
63 enum
64   {
65     LNS_TOP = 0,
66     LNS_LEFT = 2,
67     LNS_BOTTOM = 4,
68     LNS_RIGHT = 6,
69
70     LNS_COUNT = 256
71   };
72
73 /* Character attributes. */
74 #define ATTR_EMPHASIS   0x100   /* Bold-face. */
75 #define ATTR_BOX        0x200   /* Line drawing character. */
76
77 /* A line of text. */
78 struct line
79   {
80     unsigned short *chars;      /* Characters and attributes. */
81     int char_cnt;               /* Length. */
82     int char_cap;               /* Allocated bytes. */
83   };
84
85 /* How to emphasize text. */
86 enum emphasis_style
87   {
88     EMPH_BOLD,                  /* Overstrike for bold. */
89     EMPH_UNDERLINE,             /* Overstrike for underlining. */
90     EMPH_NONE                   /* No emphasis. */
91   };
92
93 /* ASCII output driver extension record. */
94 struct ascii_driver_ext
95   {
96     struct pool *pool;
97
98     /* User parameters. */
99     bool headers;               /* Print headers at top of page? */
100     bool paginate;              /* Insert formfeeds? */
101     bool squeeze_blank_lines;   /* Squeeze multiple blank lines into one? */
102     enum emphasis_style emphasis; /* How to emphasize text. */
103     int tab_width;              /* Width of a tab; 0 not to use tabs. */
104
105     int page_length;            /* Page length before subtracting margins. */
106     int top_margin;             /* Top margin in lines. */
107     int bottom_margin;          /* Bottom margin in lines. */
108
109     char *box[LNS_COUNT];       /* Line & box drawing characters. */
110     char *init;                 /* Device initialization string. */
111
112     /* Internal state. */
113     char *file_name;            /* Output file name. */
114     FILE *file;                 /* Output file. */
115     int page_number;            /* Current page number. */
116     struct line *lines;         /* Page content. */
117     int line_cap;               /* Number of lines allocated. */
118   };
119
120 static void ascii_flush (struct outp_driver *);
121 static int get_default_box_char (size_t idx);
122 static bool handle_option (struct outp_driver *this, const char *key,
123                            const struct string *val);
124
125 static bool
126 ascii_open_driver (struct outp_driver *this, struct substring options)
127 {
128   struct ascii_driver_ext *x;
129   int i;
130
131   this->width = 79;
132   this->font_height = 1;
133   this->prop_em_width = 1;
134   this->fixed_width = 1;
135   for (i = 0; i < OUTP_L_COUNT; i++)
136     this->horiz_line_width[i] = this->vert_line_width[i] = i != OUTP_L_NONE;
137
138   this->ext = x = pool_create_container (struct ascii_driver_ext, pool);
139   x->headers = true;
140   x->paginate = true;
141   x->squeeze_blank_lines = false;
142   x->emphasis = EMPH_BOLD;
143   x->tab_width = 8;
144   x->page_length = 66;
145   x->top_margin = 2;
146   x->bottom_margin = 2;
147   for (i = 0; i < LNS_COUNT; i++)
148     x->box[i] = NULL;
149   x->init = NULL;
150   x->file_name = pool_strdup (x->pool, "pspp.list");
151   x->file = NULL;
152   x->page_number = 0;
153   x->lines = NULL;
154   x->line_cap = 0;
155
156   if (!outp_parse_options (options, handle_option, this))
157     goto error;
158
159   this->length = x->page_length - x->top_margin - x->bottom_margin - 1;
160   if (x->headers)
161     this->length -= 3;
162
163   if (this->width < 59 || this->length < 15)
164     {
165       error (0, 0,
166              _("ascii: page excluding margins and headers "
167                "must be at least 59 characters wide by 15 lines long, but as "
168                "configured is only %d characters by %d lines"),
169              this->width, this->length);
170       return false;
171     }
172
173   for (i = 0; i < LNS_COUNT; i++)
174     if (x->box[i] == NULL)
175       {
176         char s[2];
177         s[0] = get_default_box_char (i);
178         s[1] = '\0';
179         x->box[i] = pool_strdup (x->pool, s);
180       }
181
182   return true;
183
184  error:
185   pool_destroy (x->pool);
186   return false;
187 }
188
189 static int
190 get_default_box_char (size_t idx)
191 {
192   /* Disassemble IDX into components. */
193   unsigned top = (idx >> LNS_TOP) & 3;
194   unsigned left = (idx >> LNS_LEFT) & 3;
195   unsigned bottom = (idx >> LNS_BOTTOM) & 3;
196   unsigned right = (idx >> LNS_RIGHT) & 3;
197
198   /* Reassemble components into nibbles in the order TLBR.
199      This makes it easy to read the case labels. */
200   unsigned value = (top << 12) | (left << 8) | (bottom << 4) | (right << 0);
201   switch (value)
202     {
203     case 0x0000:
204       return ' ';
205
206     case 0x0100: case 0x0101: case 0x0001:
207       return '-';
208
209     case 0x1000: case 0x1010: case 0x0010:
210       return '|';
211
212     case 0x0300: case 0x0303: case 0x0003:
213     case 0x0200: case 0x0202: case 0x0002:
214       return '=';
215
216     default:
217       return left > 1 || top > 1 || right > 1 || bottom > 1 ? '#' : '+';
218     }
219 }
220
221 static bool
222 ascii_close_driver (struct outp_driver *this)
223 {
224   struct ascii_driver_ext *x = this->ext;
225
226   ascii_flush (this);
227   pool_detach_file (x->pool, x->file);
228   pool_destroy (x->pool);
229
230   return true;
231 }
232
233 /* Generic option types. */
234 enum
235   {
236     boolean_arg,
237     emphasis_arg,
238     nonneg_int_arg,
239     pos_int_arg,
240     output_file_arg,
241     string_arg
242   };
243
244 static const struct outp_option option_tab[] =
245   {
246     {"headers", boolean_arg, 0},
247     {"paginate", boolean_arg, 1},
248     {"squeeze", boolean_arg, 2},
249
250     {"emphasis", emphasis_arg, 0},
251
252     {"output-file", output_file_arg, 0},
253
254     {"length", pos_int_arg, 0},
255     {"width", pos_int_arg, 1},
256
257     {"top-margin", nonneg_int_arg, 0},
258     {"bottom-margin", nonneg_int_arg, 1},
259     {"tab-width", nonneg_int_arg, 2},
260
261     {"init", string_arg, 0},
262
263     {NULL, 0, 0},
264   };
265
266 static bool
267 handle_option (struct outp_driver *this, const char *key,
268                const struct string *val)
269 {
270   struct ascii_driver_ext *x = this->ext;
271   int subcat;
272   const char *value;
273
274   value = ds_cstr (val);
275   if (!strncmp (key, "box[", 4))
276     {
277       char *tail;
278       int indx = strtol (&key[4], &tail, 4);
279       if (*tail != ']' || indx < 0 || indx > LNS_COUNT)
280         {
281           error (0, 0, _("ascii: bad index value for `box' key: syntax "
282                          "is box[INDEX], 0 <= INDEX < %d decimal, with INDEX "
283                          "expressed in base 4"),
284                  LNS_COUNT);
285           return false;
286         }
287       if (x->box[indx] != NULL)
288         error (0, 0, _("ascii: multiple values for %s"), key);
289       x->box[indx] = pool_strdup (x->pool, value);
290       return true;
291     }
292
293   switch (outp_match_keyword (key, option_tab, &subcat))
294     {
295     case -1:
296       error (0, 0, _("ascii: unknown parameter `%s'"), key);
297       break;
298     case output_file_arg:
299       x->file_name = pool_strdup (x->pool, value);
300       break;
301     case pos_int_arg:
302       {
303         char *tail;
304         int arg;
305
306         errno = 0;
307         arg = strtol (value, &tail, 0);
308         if (arg < 1 || errno == ERANGE || *tail)
309           {
310             error (0, 0, _("ascii: positive integer required as `%s' value"),
311                    key);
312             break;
313           }
314         switch (subcat)
315           {
316           case 0:
317             x->page_length = arg;
318             break;
319           case 1:
320             this->width = arg;
321             break;
322           default:
323             NOT_REACHED ();
324           }
325       }
326       break;
327     case emphasis_arg:
328       if (!strcmp (value, "bold"))
329         x->emphasis = EMPH_BOLD;
330       else if (!strcmp (value, "underline"))
331         x->emphasis = EMPH_UNDERLINE;
332       else if (!strcmp (value, "none"))
333         x->emphasis = EMPH_NONE;
334       else
335         error (0, 0,
336                _("ascii: `emphasis' value must be `bold', "
337                  "`underline', or `none'"));
338       break;
339     case nonneg_int_arg:
340       {
341         char *tail;
342         int arg;
343
344         errno = 0;
345         arg = strtol (value, &tail, 0);
346         if (arg < 0 || errno == ERANGE || *tail)
347           {
348             error (0, 0,
349                    _("ascii: zero or positive integer required as `%s' value"),
350                    key);
351             break;
352           }
353         switch (subcat)
354           {
355           case 0:
356             x->top_margin = arg;
357             break;
358           case 1:
359             x->bottom_margin = arg;
360             break;
361           case 2:
362             x->tab_width = arg;
363             break;
364           default:
365             NOT_REACHED ();
366           }
367       }
368       break;
369     case boolean_arg:
370       {
371         bool setting;
372         if (!strcmp (value, "on") || !strcmp (value, "true")
373             || !strcmp (value, "yes") || atoi (value))
374           setting = true;
375         else if (!strcmp (value, "off") || !strcmp (value, "false")
376                  || !strcmp (value, "no") || !strcmp (value, "0"))
377           setting = false;
378         else
379           {
380             error (0, 0, _("ascii: boolean value expected for `%s'"), key);
381             return false;
382           }
383         switch (subcat)
384           {
385           case 0:
386             x->headers = setting;
387             break;
388           case 1:
389             x->paginate = setting;
390             break;
391           case 2:
392             x->squeeze_blank_lines = setting;
393             break;
394           default:
395             NOT_REACHED ();
396           }
397       }
398       break;
399     case string_arg:
400       free (x->init);
401       x->init = pool_strdup (x->pool, value);
402       break;
403     default:
404       NOT_REACHED ();
405     }
406
407   return true;
408 }
409
410 static void
411 ascii_open_page (struct outp_driver *this)
412 {
413   struct ascii_driver_ext *x = this->ext;
414   int i;
415
416   if (x->file == NULL)
417     {
418       x->file = fn_open (x->file_name, "w");
419       if (x->file == NULL)
420         {
421           error (0, errno, _("ascii: opening output file \"%s\""),
422                  x->file_name);
423           return;
424         }
425       pool_attach_file (x->pool, x->file);
426
427       if (x->init != NULL)
428         fputs (x->init, x->file);
429     }
430
431   x->page_number++;
432
433   if (this->length > x->line_cap)
434     {
435       x->lines = pool_nrealloc (x->pool,
436                                 x->lines, this->length, sizeof *x->lines);
437       for (i = x->line_cap; i < this->length; i++)
438         {
439           struct line *line = &x->lines[i];
440           line->chars = NULL;
441           line->char_cap = 0;
442         }
443       x->line_cap = this->length;
444     }
445
446   for (i = 0; i < this->length; i++)
447     x->lines[i].char_cnt = 0;
448 }
449
450 /* Ensures that at least the first LENGTH characters of line Y in
451    THIS driver identified X have been cleared out. */
452 static inline void
453 expand_line (struct outp_driver *this, int y, int length)
454 {
455   struct ascii_driver_ext *ext = this->ext;
456   struct line *line = &ext->lines[y];
457   if (line->char_cnt < length)
458     {
459       int x;
460       if (line->char_cap < length)
461         {
462           line->char_cap = MIN (length * 2, this->width);
463           line->chars = pool_nrealloc (ext->pool,
464                                        line->chars,
465                                        line->char_cap, sizeof *line->chars);
466         }
467       for (x = line->char_cnt; x < length; x++)
468         line->chars[x] = ' ';
469       line->char_cnt = length;
470     }
471 }
472
473 static void
474 ascii_line (struct outp_driver *this,
475             int x0, int y0, int x1, int y1,
476             enum outp_line_style top, enum outp_line_style left,
477             enum outp_line_style bottom, enum outp_line_style right)
478 {
479   struct ascii_driver_ext *ext = this->ext;
480   int y;
481   unsigned short value;
482
483   assert (this->page_open);
484 #if DEBUGGING
485   if (x0 < 0 || x1 > this->width || y0 < 0 || y1 > this->length)
486     {
487 #if !SUPPRESS_WARNINGS
488       printf (_("ascii: bad line (%d,%d)-(%d,%d) out of (%d,%d)\n"),
489               x0, y0, x1, y1, this->width, this->length);
490 #endif
491       return;
492     }
493 #endif
494
495   value = ((left << LNS_LEFT) | (right << LNS_RIGHT)
496            | (top << LNS_TOP) | (bottom << LNS_BOTTOM) | ATTR_BOX);
497   for (y = y0; y < y1; y++)
498     {
499       int x;
500
501       expand_line (this, y, x1);
502       for (x = x0; x < x1; x++)
503         ext->lines[y].chars[x] = value;
504     }
505 }
506
507 static void
508 text_draw (struct outp_driver *this,
509            enum outp_font font,
510            int x, int y,
511            enum outp_justification justification, int width,
512            const char *string, size_t length)
513 {
514   struct ascii_driver_ext *ext = this->ext;
515   unsigned short attr = font == OUTP_EMPHASIS ? ATTR_EMPHASIS : 0;
516
517   int line_len;
518
519   switch (justification)
520     {
521     case OUTP_LEFT:
522       break;
523     case OUTP_CENTER:
524       x += (width - length + 1) / 2;
525       break;
526     case OUTP_RIGHT:
527       x += width - length;
528       break;
529     default:
530       NOT_REACHED ();
531     }
532
533   if (y >= this->length || x >= this->width)
534     return;
535
536   if (x + length > this->width)
537     length = this->width - x;
538
539   line_len = x + length;
540
541   expand_line (this, y, line_len);
542   while (length-- > 0)
543     ext->lines[y].chars[x++] = *string++ | attr;
544 }
545
546 /* Divides the text T->S into lines of width T->H.  Sets T->V to the
547    number of lines necessary.  Actually draws the text if DRAW is
548    true. */
549 static void
550 delineate (struct outp_driver *this, const struct outp_text *text, bool draw,
551            int *width, int *height)
552 {
553   int max_width;
554   int height_left;
555
556   const char *cp = ss_data (text->string);
557
558   max_width = 0;
559   height_left = text->v;
560
561   while (height_left > 0)
562     {
563       size_t chars_left;
564       size_t line_len;
565       const char *end;
566
567       /* Initially the line is up to text->h characters long. */
568       chars_left = ss_end (text->string) - cp;
569       if (chars_left == 0)
570         break;
571       line_len = MIN (chars_left, text->h);
572
573       /* A new-line terminates the line prematurely. */
574       end = memchr (cp, '\n', line_len);
575       if (end != NULL)
576         line_len = end - cp;
577
578       /* Don't cut off words if it can be avoided. */
579       if (cp + line_len < ss_end (text->string))
580         {
581           size_t space_len = line_len;
582           while (space_len > 0 && !isspace ((unsigned char) cp[space_len]))
583             space_len--;
584           if (space_len > 0)
585             line_len = space_len;
586         }
587
588       /* Draw text. */
589       if (draw)
590         text_draw (this,
591                    text->font,
592                    text->x, text->y + (text->v - height_left),
593                    text->justification, text->h,
594                    cp, line_len);
595
596       /* Update. */
597       height_left--;
598       if (line_len > max_width)
599         max_width = line_len;
600
601       /* Next line. */
602       cp += line_len;
603       if (cp < ss_end (text->string) && isspace ((unsigned char) *cp))
604         cp++;
605     }
606
607   if (width != NULL)
608     *width = max_width;
609   if (height != NULL)
610     *height = text->v - height_left;
611 }
612
613 static void
614 ascii_text_metrics (struct outp_driver *this, const struct outp_text *t,
615                     int *width, int *height)
616 {
617   delineate (this, t, false, width, height);
618 }
619
620 static void
621 ascii_text_draw (struct outp_driver *this, const struct outp_text *t)
622 {
623   assert (this->page_open);
624   delineate (this, t, true, NULL, NULL);
625 }
626
627 \f
628 /* ascii_close_page () and support routines. */
629
630 /* Writes the LENGTH characters in S to OUT.  */
631 static void
632 output_line (struct outp_driver *this, const struct line *line,
633              struct string *out)
634 {
635   struct ascii_driver_ext *ext = this->ext;
636   const unsigned short *s = line->chars;
637   size_t length;
638
639   for (length = line->char_cnt; length-- > 0; s++)
640     if (*s & ATTR_BOX)
641       ds_put_cstr (out, ext->box[*s & 0xff]);
642     else
643       {
644         if (*s & ATTR_EMPHASIS)
645           {
646             if (ext->emphasis == EMPH_BOLD)
647               {
648                 ds_put_char (out, *s);
649                 ds_put_char (out, '\b');
650               }
651             else if (ext->emphasis == EMPH_UNDERLINE)
652               ds_put_cstr (out, "_\b");
653           }
654         ds_put_char (out, *s);
655       }
656 }
657
658 static void
659 append_lr_justified (struct string *out, int width,
660                      const char *left, const char *right)
661 {
662   ds_put_char_multiple (out, ' ', width);
663   if (left != NULL)
664     {
665       size_t length = MIN (strlen (left), width);
666       memcpy (ds_end (out) - width, left, length);
667     }
668   if (right != NULL)
669     {
670       size_t length = MIN (strlen (right), width);
671       memcpy (ds_end (out) - length, right, length);
672     }
673   ds_put_char (out, '\n');
674 }
675
676 static void
677 dump_output (struct outp_driver *this, struct string *out)
678 {
679   struct ascii_driver_ext *x = this->ext;
680   fwrite (ds_data (out), ds_length (out), 1, x->file);
681   ds_clear (out);
682 }
683
684 static void
685 ascii_close_page (struct outp_driver *this)
686 {
687   struct ascii_driver_ext *x = this->ext;
688   struct string out;
689   int line_num;
690
691   if (x->file == NULL)
692     return;
693
694   ds_init_empty (&out);
695
696   ds_put_char_multiple (&out, '\n', x->top_margin);
697   if (x->headers)
698     {
699       char *r1, *r2;
700
701       r1 = xasprintf (_("%s - Page %d"), get_start_date (), x->page_number);
702       r2 = xasprintf ("%s - %s" , version, host_system);
703
704       append_lr_justified (&out, this->width, outp_title, r1);
705       append_lr_justified (&out, this->width, outp_subtitle, r2);
706       ds_put_char (&out, '\n');
707
708       free (r1);
709       free (r2);
710     }
711   dump_output (this, &out);
712
713   for (line_num = 0; line_num < this->length; line_num++)
714     {
715
716       /* Squeeze multiple blank lines into a single blank line if
717          requested. */
718       if (x->squeeze_blank_lines)
719         {
720           if (line_num >= x->line_cap)
721             break;
722           if (line_num > 0
723               && x->lines[line_num].char_cnt == 0
724               && x->lines[line_num - 1].char_cnt == 0)
725             continue;
726         }
727
728       if (line_num < x->line_cap)
729         output_line (this, &x->lines[line_num], &out);
730       ds_put_char (&out, '\n');
731       dump_output (this, &out);
732     }
733
734   ds_put_char_multiple (&out, '\n', x->bottom_margin);
735   if (x->paginate)
736     ds_put_char (&out, '\f');
737
738   dump_output (this, &out);
739   ds_destroy (&out);
740 }
741
742 /* Flushes all output to the user and lets the user deal with it.
743    This is applied only to output drivers that are designated as
744    "screen" drivers that the user is interacting with in real
745    time. */
746 static void
747 ascii_flush (struct outp_driver *this)
748 {
749   struct ascii_driver_ext *x = this->ext;
750
751   if (x->file != NULL)
752     {
753       if (fn_close (x->file_name, x->file) != 0)
754         error (0, errno, _("ascii: closing output file \"%s\""),
755                x->file_name);
756       pool_detach_file (x->pool, x->file);
757       x->file = NULL;
758     }
759 }
760
761 static void
762 ascii_chart_initialise (struct outp_driver *d UNUSED, struct chart *ch)
763 {
764   error (0, 0, _("ascii: charts are unsupported by this driver"));
765   ch->lp = 0;
766 }
767
768 static void
769 ascii_chart_finalise (struct outp_driver *d UNUSED, struct chart *ch UNUSED)
770 {
771
772 }
773
774 const struct outp_class ascii_class =
775 {
776   "ascii",
777   0,
778
779   ascii_open_driver,
780   ascii_close_driver,
781
782   ascii_open_page,
783   ascii_close_page,
784   ascii_flush,
785
786   NULL,
787
788   ascii_line,
789   ascii_text_metrics,
790   ascii_text_draw,
791
792   ascii_chart_initialise,
793   ascii_chart_finalise
794 };