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