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