]> git.neil.brown.name Git - edlib.git/blob - lib-renderline.c
Move <hide> handling to lib-renderlines and fix callers.
[edlib.git] / lib-renderline.c
1 /*
2  * Copyright Neil Brown ©2015-2023 <neil@brown.name>
3  * May be distributed under terms of GPLv2 - see file:COPYING
4  *
5  * A renderline pane will take a single line of marked-up text
6  * and draw it.  The "line" may well be longer that the width
7  * of the pane, and it might then be wrapped generatinging
8  * multiple display lines.
9  *
10  * The render-lines pane will place multiple renderline panes and use
11  * them to do the drawing - resizing and moving them as necessary to fit
12  * the size of the text.  Other panes can use renderline for similar
13  * purposes.  messageline uses just one renderline.
14  *
15  * A renderline pane can sit in the normal stack and receive Refresh
16  * messages to trigger drawing, or can sit "beside" the stack with a negative
17  * 'z' value. In that can the owner need to explicitly request refresh.
18  *
19  * "render-line:set" will set the content of the line
20  * "render-line:measure" will determine layout and size given the available
21  *    width and other parameters
22  * "render-line:draw" will send drawing commands.
23  * "Refresh" does both the measure and the draw.
24  */
25
26 #define _GNU_SOURCE /*  for asprintf */
27 #include <stdio.h>
28 #include <ctype.h>
29 #include <wctype.h>
30 #include <stdint.h>
31
32 #define PANE_DATA_TYPE struct rline_data
33 #include "core.h"
34 #include "misc.h"
35
36 /* There is one render_item entry for
37  * - each string of text with all the same attributes
38  * - each individual TAB
39  * - each unknown control character
40  * - the \n \f or \0 which ends the line
41  * When word-wrap is enabled, strings of spaces get
42  * different attributes, so a different render_item entry.
43  *
44  * attributes understood at this level are:
45  *  left:nn             - left margin - in "points" (10 points per char normally)
46  *  right:nn            - right margin
47  *  tab:nn              - move to nn from left margin or -nn from right margin
48  *  rtab                - from here to next tab or eol right-aligned
49  *  center or centre    - equal space inserted here and before next
50  *  or ctab               tab-stop or margin
51  *  space-above:nn      - extra space before (wrapped) line
52  *  space-below:nn      - extra space after (wrapped) line
53  *  height:nn           - override height.  This effectively adds space above
54  *                        every individual line if the whole line is wrapped
55  *  wrap                - text with this attr can be hidden when used as a wrap
56  *                        point.  Not hidden if cursor in the region.
57  *  wrap-margin         - remember this x offset as left margin of wrapped lines
58  *  wrap-head=xx        - text is inserted at start of line when wrapped
59  *  wrap-tail=xx        - text to include at end of line when wrapped.  This
60  *                        determines how far before right margin the wrap is
61  *                        triggered.
62  *  wrap-XXXX           - attrs to apply to wrap head/tail. Anything not
63  *                        recognised has "wrap-" stripped and is used for the
64  *                        head and tail. Default is fg:blue,underline
65  *  hide                - Text is hidden if cursor is not within range.
66  *                        NOT YET IMPLEMENTED
67  *
68  * "nn" is measured in "points" which is 1/10 the nominal width of chars
69  * in the default font size, which is called "10".  A positive value is
70  * measured from the left margin or, which setting margins, from the
71  * relevant page edge.  A negative value is measures from the right margin.
72  *
73  */
74
75 /* When an entry is split for line-wrap:
76  *   'split_cnt' is count of splits (total lines - 1)
77  *   ' split_list' is offsets from start where split happens
78  *   'x' position of wrapped portions is wrap_marign or head_length
79  *   'y' position of wrapped portions increases line_height for each
80  */
81 struct render_item {
82         struct render_item *next;
83         const char      *attr safe;
84         unsigned short  *split_list;
85         unsigned short  start, len; // in rd->line
86         unsigned short  height, width;
87         signed short    x,y; /* signed so x can go negative when shifted */
88         signed short    tab;
89         unsigned short  wrap_x; /* If this item wraps, wrap_x is the margin */
90         uint8_t         split_cnt; /* Wrap happens this many times at byte
91                                     * positions in split_list */
92         uint8_t         wrap;   /* This and consecutive render_items
93                                  * with the same wrap number form an
94                                  * optional wrap point.  It is only visible
95                                  * when not wrapped, or when cursor is in
96                                  * it.
97                                  */
98         uint8_t         hide;   /* This and consecutive render_items
99                                  * with the same hide nmber form a
100                                  * hidden extent which is visible when
101                                  * the cursor is in it.
102                                  */
103         bool            wrap_margin:1;
104         bool            hidden:1;
105         bool            eol:1;
106         unsigned int    tab_cols:4; /* For \t char */
107         enum tab_align {
108                 TAB_LEFT = 0,   // No extra space (after tab stop)
109                 TAB_RIGHT,      // Add extra space here so there no space at
110                                 // next tab stop or margin
111                 TAB_CENTRE,     // Add extra space here, half of what TAB_RIGHT
112                                 // would insert
113         }               tab_align:2;
114 };
115 /* A "tab" value of 0 means left margin, and negative is measured from right
116  * margin, so we need some other value to say "no value here"
117  */
118 static const short TAB_UNSET = (1<<(14-2));
119
120 struct rline_data {
121         unsigned short  prefix_bytes, prefix_pixels;
122         short           curs_width;
123         short           left_margin, right_margin;
124         short           space_above, space_below;
125         unsigned short  line_height, min_height;
126         unsigned short  scale;
127         unsigned short  width;
128         unsigned short  ascent;
129         char            *wrap_head, *wrap_tail, *wrap_attr;
130         int             head_length, tail_length;
131         char            *line safe;
132         bool            word_wrap;
133         bool            image;
134         int             curspos;
135
136         struct xy       image_size;
137         /* These used to check is measuring is needed, or to record
138          * results of last measurement */
139         unsigned short measure_width, measure_height;
140         short measure_offset, measure_shift_left;
141         struct render_item *content;
142 };
143 #include "core-pane.h"
144
145 static void aappend(struct buf *b safe, char const *a safe)
146 {
147         const char *end = a;
148         while (*end >= ' ' && *end != ',')
149                 end++;
150         buf_concat_len(b, a, end-a);
151         buf_append(b, ',');
152 }
153
154 static void add_render(struct rline_data *rd safe,
155                        struct render_item **safe*rip safe,
156                        const char *start safe, const char *end safe,
157                        char *attr safe,
158                        short *tab safe, enum tab_align *align safe,
159                        bool *wrap_margin safe,
160                        short wrap, short hide)
161 {
162         struct render_item *ri;
163         struct render_item **riend = *rip;
164
165         alloc(ri, pane);
166         ri->attr = strdup(attr);
167         ri->start = start - rd->line;
168         ri->len = end - start;
169         ri->tab_align = *align;
170         ri->tab = *tab;
171         ri->wrap = wrap;
172         ri->hide = hide;
173         ri->wrap_margin = *wrap_margin;
174         ri->eol = !!strchr("\n\f\0", *start);
175         *tab = TAB_UNSET;
176         *align = TAB_LEFT;
177         *wrap_margin = False;
178         *riend = ri;
179         riend = &ri->next;
180         *rip = riend;
181 }
182
183 static void parse_line(struct rline_data *rd safe)
184 {
185         /* Parse out markup in line into a renderlist with
186          * global content directly in rd.
187          */
188         struct buf attr, wrapattr;
189         struct render_item *ri = NULL, **riend = &ri;
190         const char *line = rd->line;
191         bool wrap_margin = False;
192         short tab = TAB_UNSET;
193         enum tab_align align = TAB_LEFT;
194         int hide = 0, hide_num = 0, hide_depth = 0;
195         int wrap = 0, wrap_num = 0, wrap_depth = 0;
196         unsigned char c;
197
198         rd->left_margin = rd->right_margin = 0;
199         rd->space_above = rd->space_below = 0;
200         rd->min_height = 0;
201         aupdate(&rd->wrap_head, NULL);
202         aupdate(&rd->wrap_tail, NULL);
203         aupdate(&rd->wrap_attr, NULL);
204
205         if (!line) {
206                 rd->image = False;
207                 return;
208         }
209         rd->image = strstarts(line, SOH "image:");
210         if (rd->image) {
211                 rd->image_size.x = 0;
212                 return;
213         }
214         buf_init(&attr);
215         buf_init(&wrapattr);
216
217         ri = rd->content;
218         rd->content = NULL;
219         rd->measure_width = 0; // force re-measure
220         while (ri) {
221                 struct render_item *r = ri;
222                 ri = r->next;
223                 free(r->split_list);
224                 unalloc_str_safe(r->attr, pane);
225                 unalloc(r, pane);
226         }
227
228         do {
229                 const char *st = line;
230                 c = *line++;
231
232                 while (c >= ' ' &&
233                        (!rd->word_wrap || c != ' '))
234                         c = *line++;
235
236                 if (line - 1 > st || tab != TAB_UNSET) {
237                         /* All text from st to line-1 has "attr' */
238                         add_render(rd, &riend, st, line-1, buf_final(&attr),
239                                    &tab, &align, &wrap_margin, wrap, hide);
240                         st = line - 1;
241                 }
242                 switch (c) {
243                 case soh: {
244                         int old_len;
245                         const char *a, *v;
246                         st = line;
247                         /* Move 'line' over the attrs */
248                         while (*line && line[-1] != stx)
249                                 line += 1;
250
251                         /* A set of attrs begins and ends with ',' so that
252                          * ",," separates sets of attrs
253                          * An empty set will be precisely 1 ','.  We strip
254                          * "attr," as long as we can, then strip one more ',',
255                          * which should leave either a trailing comma, or an
256                          * empty string.
257                          */
258                         buf_append(&attr, ',');
259                         old_len = attr.len;
260                         foreach_attr(a, v, st, line) {
261                                 if (amatch(a, "centre") || amatch(a, "center") ||
262                                     amatch(a, "ctab")) {
263                                         if (v)
264                                                 tab = anum(v);
265                                         align = TAB_CENTRE;
266                                 } else if (amatch(a, "tab") && v) {
267                                         tab = anum(v);
268                                         align = TAB_LEFT;
269                                 } else if (amatch(a, "rtab")) {
270                                         align = TAB_RIGHT;
271                                 } else if (amatch(a, "left") && v) {
272                                         rd->left_margin = anum(v);
273                                 } else if (amatch(a, "right") && v) {
274                                         rd->right_margin = anum(v);
275                                 } else if (amatch(a, "space-above") && v) {
276                                         rd->space_above = anum(v);
277                                 } else if (amatch(a, "space-below") && v) {
278                                         rd->space_below = anum(v);
279                                 } else if (amatch(a, "height") && v) {
280                                         rd->min_height = anum(v);
281                                 } else if (amatch(a, "wrap")) {
282                                         wrap = ++wrap_num;
283                                         wrap_depth = old_len;
284                                 } else if (amatch(a, "wrap-margin")) {
285                                         wrap_margin = True;
286                                 } else if (amatch(a, "wrap-head")) {
287                                         aupdate(&rd->wrap_head, v);
288                                 } else if (amatch(a, "wrap-tail")) {
289                                         aupdate(&rd->wrap_tail, v);
290                                 } else if (aprefix(a, "wrap-")) {
291                                         aappend(&wrapattr, a+5);
292                                 } else if (amatch(a, "word-wrap")) {
293                                         if (!v || *v == '1')
294                                                 rd->word_wrap = 1;
295                                         else if (*v == '0')
296                                                 rd->word_wrap = 0;
297                                 } else if (amatch(a, "hide")) {
298                                         hide = ++hide_num;
299                                         hide_depth = old_len;
300                                 } else
301                                         aappend(&attr, a);
302                         }
303                         break;
304                         }
305                 case etx:
306                         /* strip last set of attrs */
307                         while (attr.len >= 2 &&
308                         attr.b[attr.len-1] == ',' &&
309                         attr.b[attr.len-2] != ',') {
310                                 /* strip this attr */
311                                 attr.len -= 2;
312                                 while (attr.len && attr.b[attr.len-1] != ',')
313                                         attr.len -= 1;
314                         }
315                         /* strip one more ',' */
316                         if (attr.len > 0)
317                                 attr.len -= 1;
318                         if (wrap && attr.len <= wrap_depth)
319                                 wrap = 0;
320                         if (hide && attr.len <= hide_depth)
321                                 hide = 0;
322                         break;
323                 case ack:
324                         /* Just ignore this */
325                         break;
326                 case ' ':
327                         /* This and following spaces are wrappable */
328                         st = line;
329                         while (*line == ' ')
330                                 line += 1;
331                         wrap = ++wrap_num;
332                         add_render(rd, &riend, st - 1, line, buf_final(&attr),
333                                    &tab, &align, &wrap_margin, wrap, hide);
334                         wrap = 0;
335                         break;
336                 case '\0':
337                 case '\n':
338                 case '\f':
339                 case '\t':
340                 default:
341                         /* Each tab gets an entry of its own, as does any
342                          * stray control character.
343                          * \f \n and even \0 do.  These are needed for
344                          * easy cursor placement.
345                          */
346                         add_render(rd, &riend, st, line, buf_final(&attr),
347                                    &tab, &align, &wrap_margin, wrap, hide);
348                         break;
349                 }
350         } while (c);
351
352         rd->content = ri;
353         free(attr.b);
354         if (buf_final(&wrapattr)[0])
355                 rd->wrap_attr = buf_final(&wrapattr);
356         else {
357                 free(wrapattr.b);
358                 rd->wrap_attr = strdup(",fg:blue,underline,");
359         }
360 }
361
362 static inline struct call_return do_measure(struct pane *p safe,
363                                             struct render_item *ri safe,
364                                             int splitpos, int len,
365                                             int maxwidth)
366 {
367         struct rline_data *rd = &p->data;
368         struct call_return cr;
369         char tb[] = "        ";
370         char *str = rd->line + ri->start + splitpos;
371         char tmp;
372
373         if (ri->len && rd->line[ri->start] == '\t') {
374                 str = tb;
375                 if (len < 0)
376                         len = ri->tab_cols - splitpos;
377         } else
378                 if (len < 0)
379                         len = ri->len - splitpos;
380         tmp = str[len];
381         str[len] = 0;
382
383         cr = call_ret(all, "Draw:text-size", p,
384                       maxwidth, NULL, str,
385                       rd->scale, NULL, ri->attr);
386
387         str[len] = tmp;
388         if (cr.ret == 1 && maxwidth >= 0 &&
389             cr.i >= len)
390                 /* All fits in maxwidth */
391                 cr.ret = 2;
392         /* Report position in rd->line */
393         if (str == tb) {
394                 cr.s = rd->line + ri->start;
395                 if (splitpos + cr.i >= ri->tab_cols)
396                         cr.s += 1;
397         } else
398                 cr.s = str + cr.i;
399         return cr;
400 }
401
402 static inline struct call_return measure_str(struct pane *p safe,
403                                              char *str safe,
404                                              const char *attr)
405 {
406         struct rline_data *rd = &p->data;
407
408         return call_ret(all, "Draw:text-size", p,
409                         -1, NULL, str,
410                         rd->scale, NULL, attr);
411 }
412
413 static inline void do_draw(struct pane *p safe,
414                            struct pane *focus safe,
415                            struct render_item *ri safe, int split,
416                            int offset,
417                            int x, int y)
418 {
419         struct rline_data *rd = &p->data;
420         char tmp;
421         char *str;
422         int len;
423         char tb[] = "         ";
424
425         str = rd->line + ri->start;
426         len = ri->len;
427
428         y += rd->ascent;
429         if (ri->len && strchr("\f\n\0", str[0])) {
430                 /* end marker - len extends past end of string,
431                  * but mustn't write there.  Only need to draw if
432                  * cursor is here.
433                  */
434                 if (offset == 0)
435                         home_call(focus, "Draw:text", p, offset, NULL, "",
436                                   rd->scale, NULL, ri->attr, x, y);
437                 return;
438         }
439         if (ri->len && str[0] == '\t') {
440                 len = ri->tab_cols;
441                 if (split)
442                         offset = -1;
443         }
444         if (ri->split_list) {
445                 if (split < ri->split_cnt)
446                         len = ri->split_list[split];
447                 if (split)
448                         len -= ri->split_list[split-1];
449         }
450
451         if (ri->len && str[0] == '\t')
452                 /* Tab need a list of spaces */
453                 str = tb;
454         else
455                 if (split > 0 && split <= ri->split_cnt && ri->split_list) {
456                         str += ri->split_list[split-1];
457                         offset -= ri->split_list[split-1];
458                 }
459
460         tmp = str[len];
461         str[len] = 0;
462
463         if (offset >= len)
464                 offset = -1;
465         home_call(focus, "Draw:text", p, offset, NULL, str,
466                            rd->scale, NULL, ri->attr, x, y);
467         str[len] = tmp;
468 }
469
470 static inline void draw_wrap(struct pane *p safe,
471                              struct pane *focus safe,
472                              char *str safe,
473                              int x, int y)
474 {
475         struct rline_data *rd = &p->data;
476
477         home_call(focus, "Draw:text", p,
478                   -1, NULL, str,
479                   rd->scale, NULL, rd->wrap_attr,
480                   x, y + rd->ascent);
481 }
482
483 static bool add_split(struct render_item *ri safe, int split)
484 {
485         int i = ri->split_cnt;
486         if (i > 250)
487                 return False;
488         ri->split_cnt += 1;
489         ri->split_list = realloc(ri->split_list,
490                                  sizeof(ri->split_list[0]) * ri->split_cnt);
491         ri->split_list[i] = split;
492         return True;
493 }
494
495 static int calc_pos(int num, int margin, int width)
496 {
497         if (num >= 0)
498                 return num * width / 10;
499         if (-num * width / 10 > margin)
500                 return 0;
501         return margin + num * width / 10;
502 }
503
504 static int measure_line(struct pane *p safe, struct pane *focus safe, int offset)
505 {
506         /* First measure each render_item entry setting
507          * height, ascent, width.
508          * Then use that with tab information to set 'x' position for
509          * each unit.
510          * Finally identify line-break locations if needed and set 'y'
511          * positions
512          *
513          * Return 1 if there is an EOL ('\n')
514          * 2 if there is an end-of-page ('\f')
515          * 3 if both.
516          * 0 if neither
517          */
518         struct rline_data *rd = &p->data;
519         struct render_item *ri, *wraprl;
520         int shift_left = pane_attr_get_int(focus, "render-wrap", -1);
521         bool wrap = shift_left < 0;
522         int wrap_margin;
523         int right_margin;
524         int left_margin;
525         struct xy xyscale = pane_scale(focus);
526         int curs_height;
527         int xdiff, ydiff;
528         struct call_return cr;
529         int x, y;
530         int ret = 0;
531         bool seen_rtab = False;
532         unsigned int offset_hide = 0;
533
534         if (!rd->content)
535                 return ret;
536         if (xyscale.x == rd->scale && p->w == rd->measure_width &&
537             shift_left == rd->measure_shift_left &&
538             offset == rd->measure_offset) {
539                 /* No change */
540                 for (ri = rd->content ; ri ; ri = ri->next) {
541                         if (ri->hidden)
542                                 continue;
543                         if (ri->eol && rd->line[ri->start] == '\n')
544                                 ret |= 1;
545                         if (ri->eol && rd->line[ri->start] == '\f')
546                                 ret |= 2;
547                 }
548                 pane_resize(p, p->x, p->y, p->w, rd->measure_height);
549                 return ret;
550         }
551         rd->scale = xyscale.x;
552         rd->measure_width = p->w;
553         rd->measure_offset = offset;
554         rd->measure_shift_left = shift_left;
555
556         cr = measure_str(p, "M", "");
557         rd->curs_width = cr.x;
558         curs_height = cr.y;
559         rd->line_height = cr.y;
560         rd->ascent = cr.i2;
561         if (rd->min_height > 10)
562                 rd->line_height = rd->line_height * rd->min_height / 10;
563
564         if (rd->wrap_head) {
565                 cr = measure_str(p, rd->wrap_head, rd->wrap_attr);
566                 rd->head_length = cr.x;
567         }
568         cr = measure_str(p, rd->wrap_tail ?: "\\", rd->wrap_attr);
569         rd->tail_length = cr.x;
570
571         left_margin = calc_pos(rd->left_margin, p->w, rd->curs_width);
572         /* 0 means right edge for right_margin, and left edge for all others */
573         right_margin = p->w - calc_pos(-rd->right_margin, p->w, rd->curs_width);
574
575         if (offset >= 0)
576                 for (ri = rd->content ; ri ; ri = ri->next)
577                         if (offset < ri->start + ri->len) {
578                                 offset_hide = ri->hide;
579                                 break;
580                         }
581
582         for (ri = rd->content; ri; ri = ri->next) {
583                 ri->hidden = (ri->hide && ri->hide != offset_hide);
584                 if (ri->hidden) {
585                         ri->width = 0;
586                         continue;
587                 }
588                 if (ri->len == 0 ||
589                     (unsigned char)rd->line[ri->start] >= ' ') {
590                         cr = do_measure(p, ri, 0, -1, -1);
591                 } else {
592                         char tmp[4];
593                         if (ri->eol) {
594                                 /* Ensure attributes of newline add to line
595                                  * height. The width will be ignored. */
596                                 strcpy(tmp, "M");
597                                 if (rd->line[ri->start] == '\n')
598                                         ret |= 1;
599                                 if (rd->line[ri->start] == '\f')
600                                         ret |= 2;
601                         } else if (rd->line[ri->start] == '\t') {
602                                 strcpy(tmp, " ");
603                         } else {
604                                 strcpy(tmp, "^x");
605                                 tmp[1] = '@' + (rd->line[ri->start] & 31);
606                         }
607                         cr = measure_str(p, tmp, ri->attr);
608                 }
609
610                 if (cr.y > rd->line_height)
611                         rd->line_height = cr.y;
612                 ri->height = cr.y;
613                 if (cr.i2 > rd->ascent)
614                         rd->ascent = cr.i2;
615                 ri->width = ri->eol ? 0 : cr.x;
616
617                 if (ri->start <= offset && offset <= ri->start + ri->len) {
618                         cr = measure_str(p, "M", ri->attr);
619                         rd->curs_width = cr.x;
620                 }
621
622                 ri->split_cnt = 0;
623                 free(ri->split_list);
624                 ri->split_list = NULL;
625         }
626         /* Set 'x' position honouring tab stops, and set length
627          * of "\t" characters.  Also handle \n and \f.
628          */
629         x = left_margin - (shift_left > 0 ? shift_left : 0);
630         y = rd->space_above * curs_height / 10;
631         rd->width = 0;
632         for (ri = rd->content; ri; ri = ri->next) {
633                 int w, margin;
634                 struct render_item *ri2;
635                 ri->y = y;
636                 if (ri->hidden) {
637                         ri->x = x;
638                         continue;
639                 }
640                 if (ri->tab != TAB_UNSET)
641                         x =  left_margin + calc_pos(ri->tab,
642                                                     right_margin - left_margin,
643                                                     rd->curs_width);
644                 if (ri->eol) {
645                         /* EOL */
646                         if (x > rd->width)
647                                 rd->width = x;
648                         ri->x = x;
649                         x = 0; /* Don't include shift. probably not margin */
650                         if (rd->line[ri->start])
651                                 y += rd->line_height;
652                         continue;
653                 }
654                 if (ri->tab_align == TAB_LEFT) {
655                         ri->x = x;
656                         if (ri->len && rd->line[ri->start] == '\t') {
657                                 int col = x / ri->width;
658                                 int cols= 8 - (col % 8);
659                                 ri->tab_cols = cols;
660                                 ri->width *= cols;
661                         }
662                         x += ri->width;
663                         continue;
664                 }
665                 if (ri->tab_align == TAB_RIGHT)
666                         seen_rtab = True;
667                 w = ri->width;
668                 for (ri2 = ri->next;
669                      ri2 && ri2->tab_align == TAB_LEFT && ri2->tab == TAB_UNSET;
670                      ri2 = ri2->next)
671                         w += ri2->width;
672                 while (ri2 && ri2->tab == TAB_UNSET)
673                         ri2 = ri2->next;
674                 margin = right_margin - left_margin;
675                 if (ri2)
676                         margin =  left_margin + calc_pos(ri2->tab,
677                                                          right_margin - left_margin,
678                                                          rd->curs_width);
679                 if (ri->tab_align == TAB_RIGHT)
680                         x = x + margin - x - w;
681                 else
682                         x = x + (margin - x - w) / 2;
683                 ri->x = x;
684                 while (ri->next && ri->next->next && ri->next->tab_align == TAB_LEFT) {
685                         x += ri->width;
686                         ri = ri->next;
687                         ri->x = x;
688                         ri->y = y;
689                 }
690         }
691
692         /* Now we check to see if the line needs to be wrapped and
693          * if so, adjust some y values and reduce x.  If we need to
694          * split an individual entry we create an array of split points.
695          */
696         xdiff = 0; ydiff = 0; y = 0;
697         wraprl = NULL;
698         wrap_margin = left_margin + rd->head_length;
699         for (ri = rd->content ; wrap && ri ; ri = ri->next) {
700                 int splitpos;
701                 if (ri->hidden)
702                         continue;
703                 if (ri->wrap && (wraprl == NULL || ri->wrap != wraprl->wrap))
704                         wraprl = ri;
705                 if (ri->wrap_margin)
706                         wrap_margin = ri->x + xdiff;
707                 ri->wrap_x = wrap_margin;
708                 ri->x += xdiff;
709                 ri->y += ydiff;
710                 y = ri->y;
711                 if (ri->eol) {
712                         xdiff = 0;
713                         continue;
714                 }
715                 if (ri->x + ri->width <= right_margin - rd->tail_length)
716                         continue;
717                 if ((ri->next == NULL || ri->next->eol) &&
718                     ri->x + ri->width <= right_margin &&
719                     seen_rtab)
720                         /* Don't need any tail space for last item.
721                          * This allows rtab to fully right-justify,
722                          * but leaves no-where for the cursor.  So
723                          * only do it if rtab is present.
724                          */
725                         continue;
726                 /* This doesn't fit here */
727                 if (wraprl) {
728                         /* Move wraprl to next line and hide it unless it contains cursor */
729                         int xd;
730                         struct render_item *wraprl2, *ri2;
731
732                         /* Find last ritem in wrap region.*/
733                         for (wraprl2 = wraprl ;
734                              wraprl2->next && wraprl2->next->wrap == wraprl->wrap ;
735                              wraprl2 = wraprl2->next)
736                                 ;
737                         wrap_margin = wraprl2->wrap_x;
738                         if (wraprl2->next) {
739                                 xd = wraprl2->next->x - wrap_margin;
740                                 if (wraprl2->next->start > ri->start)
741                                         xd += xdiff;
742                         } else {
743                                 xd = wraprl2->x - wrap_margin;
744                                 if (wraprl2->start > ri->start)
745                                         xd += xdiff;
746                         }
747                         if (offset >= 0 &&
748                             offset >= wraprl->start &&
749                             offset <= wraprl2->start + wraprl2->len) {
750                                 /* Cursor is here, so keep it visible.
751                                  * If we are still in the wrap region, pretend
752                                  * it didn't exist, else move first item
753                                  * after it to next line
754                                  */
755                                 if (ri->wrap == wraprl->wrap)
756                                         goto normal_wrap;
757                         } else {
758                                 /* Hide the wrap region */
759                                 while (wraprl != wraprl2) {
760                                         wraprl->hidden = True;
761                                         wraprl = wraprl->next;
762                                 }
763                                 if (wraprl)
764                                         wraprl->hidden = True;
765                                 while (ri->next && ri->next->hidden)
766                                         ri = ri->next;
767                         }
768                         for (ri2 = wraprl2->next ; ri2 && ri2 != ri->next; ri2 = ri2->next) {
769                                 ri2->y += rd->line_height;
770                                 ri2->x -= xd;
771                                 if (ri2->wrap_margin)
772                                         wrap_margin = ri2->x;
773                                 ri2->wrap_x = wrap_margin;
774                         }
775                         xdiff -= xd;
776                         ydiff += rd->line_height;
777                         wraprl = NULL;
778                         if (ri->hidden ||
779                             ri->x + ri->width <= right_margin - rd->tail_length)
780                                 continue;
781                 }
782         normal_wrap:
783                 /* Might need to split this ri into two or more pieces */
784                 x = ri->x;
785                 splitpos = 0;
786                 while (1) {
787                         cr = do_measure(p, ri, splitpos, -1,
788                                         right_margin - rd->tail_length - x);
789                         if (cr.ret == 2)
790                                 /* Remainder fits now */
791                                 break;
792                         if (cr.i == 0 && splitpos == 0) {
793                                 /* None of this fits here, move to next line */
794                                 xdiff -= ri->x - wrap_margin;
795                                 ri->x = wrap_margin;
796                                 x = ri->x;
797                                 ydiff += rd->line_height;
798                                 ri->y += rd->line_height;
799                                 wraprl = NULL;
800                         }
801                         if (cr.i == 0)
802                                 /* Nothing fits and we already split - give up */
803                                 break;
804                         /* re-measure the first part */
805                         cr = do_measure(p, ri, splitpos,
806                                         cr.i,
807                                         right_margin - rd->tail_length - x);
808                         ydiff += rd->line_height;
809                         xdiff -= cr.x;
810                         if (splitpos == 0) {
811                                 xdiff -= ri->x - wrap_margin;
812                                 x = wrap_margin;
813                         }
814                         splitpos += cr.i;
815                         if (!add_split(ri, splitpos))
816                                 break;
817                 }
818         }
819         rd->measure_height =
820                 (rd->space_above + rd->space_below) * curs_height / 10 +
821                 ydiff + rd->line_height;
822         pane_resize(p, p->x, p->y, p->w, rd->measure_height);
823         attr_set_int(&p->attrs, "line-height", rd->line_height);
824         return ret;
825 }
826
827 static void draw_line(struct pane *p safe, struct pane *focus safe, int offset)
828 {
829         struct rline_data *rd = &p->data;
830         struct render_item *ri;
831         char *wrap_tail = rd->wrap_tail ?: "\\";
832         char *wrap_head = rd->wrap_head ?: "";
833
834         home_call(focus, "Draw:clear", p);
835
836         if (!rd->content)
837                 return;
838         for (ri = rd->content ; ri; ri = ri->next) {
839                 int split = 0;
840                 short y = ri->y;
841                 int cpos;
842
843                 if (ri->hidden)
844                         continue;
845                 if (offset < 0 || offset >= ri->start + ri->len)
846                         cpos = -1;
847                 else if (offset < ri->start)
848                         cpos = 0;
849                 else
850                         cpos = offset - ri->start;
851
852                 do_draw(p, focus, ri, 0, cpos, ri->x, y);
853
854                 while (split < ri->split_cnt ||
855                        (ri->next && !ri->next->eol && ri->next->y > y)) {
856                         /* line wrap here */
857                         /* don't show head/tail for wrap-regions */
858                         if (*wrap_tail /*&& !ri->wrap*/)
859                                 draw_wrap(p, focus, wrap_tail,
860                                           p->w - rd->tail_length, y);
861                         y += rd->line_height;
862                         if (*wrap_head /*&& !ri->wrap*/)
863                                 draw_wrap(p, focus, wrap_head,
864                                           0, y);
865                         if (ri->split_list && split < ri->split_cnt) {
866                                 split += 1;
867                                 do_draw(p, focus, ri, split, cpos,
868                                         ri->wrap_x,
869                                         y);
870                         } else
871                                 break;
872                 }
873                 if (offset < ri->start + ri->len)
874                         offset = -1;
875         }
876 }
877
878 static int find_xy(struct pane *p safe, struct pane *focus safe,
879                    short x, short y, const char **xyattr)
880 {
881         /* Find the location in ->line that is best match for x,y.
882          * If x,y is on the char at that location, when store attributes
883          * for the char in xyattr
884          * We always return a location, even if no xyattr.
885          * We use the last render_item that is not definitely after x,y
886          * We do not consider the eol render_item
887          */
888         struct call_return cr;
889         struct rline_data *rd = &p->data;
890         struct render_item *r, *ri = NULL;
891         int splitpos = 0;
892         int start = 0;
893
894         for (r = rd->content; r ; r = r->next) {
895                 int split;
896                 if (r->hidden)
897                         continue;
898                 if (r->y <= y && r->x <= x) {
899                         ri = r;
900                         start = r->start;
901                         splitpos = 0;
902                 }
903                 for (split = 0; split < r->split_cnt; split++) {
904                         if (r->y + (split + 1) * rd->line_height <= y &&
905                             r->wrap_x <= x && r->split_list) {
906                                 ri = r;
907                                 splitpos = r->split_list[split];
908                                 start = r->start + splitpos;
909                         }
910                 }
911         }
912         if (!ri)
913                 return 0;
914         if (ri->eol)
915                 /* newline or similar.  Can only be at start */
916                 return start;
917         cr = do_measure(p, ri, splitpos, -1, x - ri->x);
918         if ((splitpos ? ri->wrap_x : ri->x ) + cr.x > x &&
919             ri->y + rd->line_height * (1 + splitpos) > y &&
920             xyattr) {
921                 /* This is a bit of a hack.
922                  * We stick the x,y co-ords of the start
923                  * of the current attr in front of the
924                  * attrs so render-lines can provide a
925                  * good location for a menu
926                  */
927                 char buf[100];
928                 struct render_item *ri2;
929                 int ax = ri->x;
930                 for (ri2 = rd->content; ri2 != ri; ri2 = ri2->next)
931                         if (strcmp(ri2->attr, ri->attr) == 0)
932                                 ax = ri2->x;
933                 snprintf(buf, sizeof(buf), "%dx%d,", ax, y);
934                 *xyattr = strconcat(p, buf, ri->attr);
935         }
936         if (cr.s)
937                 return cr.s - rd->line;
938         return start + cr.i;
939 }
940
941 static struct xy find_curs(struct pane *p safe, int offset, const char **cursattr)
942 {
943         struct call_return cr;
944         struct xy xy = {0,0};
945         int split;
946         int st;
947         struct rline_data *rd = &p->data;
948         struct render_item *r, *ri = NULL;
949
950         for (r = rd->content; r; r = r->next) {
951                 if (r->hidden)
952                         continue;
953                 if (offset < r->start)
954                         break;
955                 ri = r;
956         }
957         if (!ri) {
958                 /* This should be impossible as the eol goes past
959                  * the largest offset.
960                  */
961                 return xy;
962         }
963         if (offset < ri->start)
964                 /* in the attrs?? */
965                 offset = 0;
966         else
967                 offset -= ri->start;
968         /* offset now from ri->start */
969         if (ri->len && rd->line[ri->start] == '\t' && offset)
970                 offset = ri->tab_cols;
971         if (cursattr)
972                 *cursattr = ri->attr;
973         st = 0;
974         for (split = 0; split < ri->split_cnt && ri->split_list; split ++) {
975                 if (offset < ri->split_list[split])
976                         break;
977                 st = ri->split_list[split];
978         }
979         if (ri->eol)
980                 cr.x = offset ? ri->width : 0;
981         else
982                 cr = do_measure(p, ri, st, offset - st, -1);
983
984         if (split)
985                 xy.x = ri->wrap_x + cr.x;
986         else
987                 xy.x = ri->x + cr.x;
988         xy.y = ri->y + split * rd->line_height;
989         if (ri->next == NULL && offset > ri->len) {
990                 /* After the newline ? Go to next line */
991                 xy.x = 0;
992                 xy.y += rd->line_height;
993         }
994         return xy;
995 }
996
997 static void parse_map(const char *map safe, short *rowsp safe, short *colsp safe)
998 {
999         /* The map must be a sequence of rows, each of which is
1000          * a sequence of chars starting CAPS and continuing lower.
1001          * Each row must be the same length.
1002          */
1003         short cols = -1;
1004         short rows = 0;
1005         short this_cols = 0;
1006
1007         for (; *map && isalpha(*map); map += 1) {
1008                 if (isupper(*map)) {
1009                         if (rows > 1)
1010                                 if (this_cols != cols)
1011                                         /* Rows aren't all the same */
1012                                         return;
1013                         if (rows)
1014                                 cols = this_cols;
1015
1016                         this_cols = 1;
1017                         rows += 1;
1018                 } else if (rows == 0) {
1019                         /* First row malformed */
1020                         return;
1021                 } else {
1022                         this_cols += 1;
1023                 }
1024         }
1025         if (this_cols != cols)
1026                 /* Last row is wrong length */
1027                 return;
1028         *rowsp = rows;
1029         *colsp = cols;
1030 }
1031
1032 static int render_image(struct pane *p safe, struct pane *focus safe,
1033                         const char *line safe,
1034                         int dodraw,
1035                         int offset, int want_xypos, short x, short y)
1036 {
1037         struct rline_data *rd = &p->data;
1038         char *fname = NULL;
1039         const char *orig_line = line;
1040         short width, height;
1041         short rows = -1, cols = -1;
1042         int map_offset = 0;
1043         int ioffset;
1044         struct xy xyscale = pane_scale(focus);
1045         int scale = xyscale.x;
1046         struct xy size= {-1, -1};
1047         const char *a, *v;
1048         const char *end;
1049
1050         if (dodraw)
1051                 home_call(focus, "Draw:clear", p);
1052
1053         width = p->parent->w/2;
1054         height = p->parent->h/2;
1055
1056         while (*line == soh)
1057                 line += 1;
1058
1059         end = line + strcspn(line, STX);
1060         foreach_attr(a, v, line, end) {
1061                 if (amatch(a, "image") && v) {
1062                         aupdate(&fname, v);
1063                         if (rd->image_size.x <= 0) {
1064                                 struct call_return cr =
1065                                         home_call_ret(all, focus,
1066                                                       "Draw:image-size",
1067                                                       p, 0, NULL, fname);
1068                                 if (cr.x > 0 && cr.y > 0) {
1069                                         size.x = cr.x;
1070                                         size.y = cr.y;
1071                                         rd->image_size = size;
1072                                 }
1073                         } else
1074                                 size = rd->image_size;
1075                 } else if (amatch(a, "width") && v) {
1076                         width = anum(v) * scale / 1000;
1077                 } else if (amatch(a, "height") && v) {
1078                         height = anum(v) * scale / 1000;
1079                 } else if (amatch(a, "noupscale") &&
1080                            fname && size.x > 0) {
1081                         if (size.x < p->parent->w)
1082                                 width = size.x;
1083                         if (size.y < p->parent->h)
1084                                 height = size.y;
1085                 } else if ((offset >= 0 || want_xypos) &&
1086                            amatch(a, "map") && v) {
1087                         /*
1088                          * A map is map:LxxxLxxxLxxxLxxx or similar
1089                          * Where each "Lxxx" recognised by a CAP followed
1090                          * by lower is a row, and each char is a column.
1091                          * So we count the CAPs to get row count, and
1092                          * count the chars to get col count.
1093                          * If want_xypos then map x,y ino that matrix
1094                          * and return pos in original line of cell.
1095                          * If offset is in the map, then set ->cx,->cy to
1096                          * the appropriate location.
1097                          */
1098                         map_offset = v - orig_line;
1099                         parse_map(v, &rows, &cols);
1100                 }
1101         }
1102         pane_resize(p, (p->parent->w - width)/2, p->y, width, height);
1103
1104         attr_set_int(&p->attrs, "line-height", p->h);
1105
1106         /* Adjust size to be the scaled size - it must fit in
1107          * p->w, p->h
1108          */
1109         if (size.x * p->h > size.y * p->w) {
1110                 /* Image is wider than space */
1111                 size.y = size.y * p->w / size.x;
1112                 size.x = p->w;
1113                 ioffset = 0;
1114         } else {
1115                 /* Image is taller than space */
1116                 size.x = size.x * p->h / size.y;
1117                 size.y = p->h;
1118                 ioffset = (p->w - size.x) / 2;
1119         }
1120
1121         p->cx = p->cy = -1;
1122
1123         if (offset >= 0 && map_offset > 0 && rows > 0 &&
1124             offset >= map_offset && offset < map_offset + (rows*cols)) {
1125                 /* Place cursor based on where 'offset' is in the map */
1126                 short r = (offset - map_offset) / cols;
1127                 short c = offset - map_offset - r * cols;
1128                 p->cx = size.x / cols * c + ioffset;
1129                 p->cy = size.y / rows * r;
1130         }
1131
1132         if (fname && dodraw)
1133                 home_call(focus, "Draw:image", p, 5, NULL, fname,
1134                           0, NULL, NULL, cols, rows);
1135
1136         free(fname);
1137
1138         if (want_xypos && map_offset > 0 && rows > 0) {
1139                 /* report where x,y is as a position in the map */
1140                 short r = y * rows / size.y;
1141                 short c = (x > ioffset ? x - ioffset : 0) * cols / size.x;
1142                 if (c >= cols) c = cols - 1;
1143                 /* +1 below because result must never be zero */
1144                 return map_offset + r * cols + c + 1;
1145         }
1146         return 1;
1147 }
1148
1149 DEF_CMD(renderline_draw)
1150 {
1151         struct rline_data *rd = &ci->home->data;
1152         struct xy xy;
1153         int offset = -1;
1154
1155         if (ci->num >= 0)
1156                 offset = rd->prefix_bytes + ci->num;
1157
1158         if (rd->image)
1159                 render_image(ci->home, ci->focus, rd->line, True,
1160                              offset, False, 0, 0);
1161         else
1162                 draw_line(ci->home, ci->focus, offset);
1163
1164         if (ci->num >= 0) {
1165                 xy = find_curs(ci->home, rd->prefix_bytes + ci->num, NULL);
1166                 ci->home->cx = xy.x;
1167                 ci->home->cy = xy.y;
1168         }
1169         return 1;
1170 }
1171
1172 DEF_CMD(renderline_refresh)
1173 {
1174         struct rline_data *rd = &ci->home->data;
1175         int offset = -1;
1176
1177         if (rd->curspos >= 0)
1178                 offset = rd->prefix_bytes + rd->curspos;
1179         if (rd->image)
1180                 render_image(ci->home, ci->focus, rd->line, True,
1181                              offset, False, 0, 0);
1182         else {
1183                 measure_line(ci->home, ci->focus, offset);
1184                 draw_line(ci->home, ci->focus, offset);
1185         }
1186         return 1;
1187 }
1188
1189 DEF_CMD(renderline_measure)
1190 {
1191         struct rline_data *rd = &ci->home->data;
1192         int ret;
1193
1194         if (rd->image)
1195                 return render_image(ci->home, ci->focus, rd->line,
1196                                     False, ci->num, False, 0, 0);
1197
1198         ret = measure_line(ci->home, ci->focus,
1199                            ci->num < 0 ? -1 : rd->prefix_bytes + ci->num);
1200         rd->prefix_pixels = 0;
1201         if (rd->prefix_bytes) {
1202                 struct xy xy = find_curs(ci->home, rd->prefix_bytes, NULL);
1203                 rd->prefix_pixels = xy.x;
1204         }
1205         if (ci->num >= 0) {
1206                 /* Find cursor and report x,y pos and attributes */
1207                 const char *cursattr = NULL;
1208                 struct xy xy;
1209                 xy = find_curs(ci->home, rd->prefix_bytes + ci->num, &cursattr);
1210                 comm_call(ci->comm2, "cb", ci->focus, ret, NULL,
1211                           cursattr);
1212                 ci->home->cx = xy.x;
1213                 ci->home->cy = xy.y;
1214         }
1215         return ret | 4;
1216 }
1217
1218 DEF_CMD(renderline_findxy)
1219 {
1220         struct rline_data *rd = &ci->home->data;
1221         const char *xyattr = NULL;
1222         int pos;
1223
1224         if (rd->image)
1225                 return render_image(ci->home, ci->focus, rd->line,
1226                                     False, -1, True,
1227                                     ci->x, ci->y);
1228
1229         measure_line(ci->home, ci->focus,
1230                      ci->num < 0 ? -1 : rd->prefix_bytes + ci->num);
1231         pos = find_xy(ci->home, ci->focus, ci->x, ci->y, &xyattr);
1232         if (pos >= rd->prefix_bytes)
1233                 pos -= rd->prefix_bytes;
1234         else {
1235                 pos = 0;
1236                 xyattr = NULL;
1237         }
1238         comm_call(ci->comm2, "cb", ci->focus, pos, NULL, xyattr);
1239         return pos+1;
1240 }
1241
1242 DEF_CMD(renderline_get)
1243 {
1244         struct rline_data *rd = &ci->home->data;
1245         char buf[20];
1246         const char *val = buf;
1247
1248         if (!ci->str)
1249                 return Enoarg;
1250         if (strcmp(ci->str, "prefix_len") == 0)
1251                 snprintf(buf, sizeof(buf), "%d", rd->prefix_pixels);
1252         else if (strcmp(ci->str, "curs_width") == 0)
1253                 snprintf(buf, sizeof(buf), "%d", rd->curs_width);
1254         else if (strcmp(ci->str, "width") == 0)
1255                 snprintf(buf, sizeof(buf), "%d", rd->width);
1256         else
1257                 return Einval;
1258
1259         comm_call(ci->comm2, "attr", ci->focus, 0, NULL, val);
1260         return 1;
1261 }
1262
1263 static char *cvt(char *str safe)
1264 {
1265         /* Convert:
1266          *    << to < ack  (ack is a no-op)
1267          *    < stuff > to soh stuff stx
1268          *    </> to ack ack etx
1269          */
1270         char *c, *c1;
1271         for (c = str; *c; c += 1) {
1272                 if (c[0] == soh || c[0] == ack)
1273                         break;
1274                 if (c[0] == '<' && c[1] == '<') {
1275                         c[1] = ack;
1276                         c++;
1277                         continue;
1278                 }
1279                 if (c[0] != '<')
1280                         continue;
1281                 if (c[1] == '/') {
1282                         while (*c && *c != '>')
1283                                 *c++ = ack;
1284                         if (!*c)
1285                                 break;
1286                         *c = etx;
1287                         continue;
1288                 }
1289                 c[0] = soh;
1290                 c += 1;
1291                 c1 = c;
1292                 while (*c && *c != '>') {
1293                         if (*c == '\\' &&
1294                             (c[1] == '\\' || c[1] == '>'))
1295                                 c++;
1296                         *c1++ = *c++;
1297                 }
1298                 while (c1 < c)
1299                         *c1++ = ack;
1300                 if (!*c)
1301                         break;
1302                 *c = stx;
1303         }
1304         return str;
1305 }
1306
1307 DEF_CMD(renderline_set)
1308 {
1309         struct rline_data *rd = &ci->home->data;
1310         const char *old = rd->line;
1311         char *prefix = pane_attr_get(ci->focus, "prefix");
1312         bool word_wrap = pane_attr_get_int(ci->focus, "word-wrap", 0) != 0;
1313
1314         if (!ci->str)
1315                 return -Enoarg;
1316         if (prefix)
1317                 prefix = cvt(strconcat(ci->home, "<bold>", prefix, "</>"));
1318
1319         if (prefix)
1320                 rd->line = strconcat(NULL, prefix, ci->str);
1321         else
1322                 rd->line = strdup(ci->str);
1323         rd->prefix_bytes = strlen(prefix?:"");
1324         cvt(rd->line + rd->prefix_bytes);
1325
1326         rd->curspos = ci->num;
1327         if (strcmp(rd->line, old) != 0 ||
1328             (old && word_wrap != rd->word_wrap)) {
1329                 pane_damaged(ci->home, DAMAGED_REFRESH);
1330                 pane_damaged(ci->home->parent, DAMAGED_REFRESH);
1331                 rd->word_wrap = word_wrap;
1332                 parse_line(rd);
1333         }
1334         free((void*)old);
1335         ci->home->damaged &= ~DAMAGED_VIEW;
1336         return 1;
1337 }
1338
1339 DEF_CMD(renderline_close)
1340 {
1341         struct rline_data *rd = &ci->home->data;
1342         struct render_item *ri = rd->content;
1343
1344         free((void*)rd->line);
1345         while (ri) {
1346                 struct render_item *r = ri;
1347                 ri = r->next;
1348                 free(r->split_list);
1349                 unalloc_str_safe(r->attr, pane);
1350                 unalloc(r, pane);
1351         }
1352         aupdate(&rd->wrap_head, NULL);
1353         aupdate(&rd->wrap_tail, NULL);
1354         aupdate(&rd->wrap_attr, NULL);
1355         return 1;
1356 }
1357
1358 static struct map *rl_map;
1359 DEF_LOOKUP_CMD(renderline_handle, rl_map);
1360
1361 DEF_CMD(renderline_attach)
1362 {
1363         struct pane *p;
1364         struct rline_data *rd;
1365
1366         if (!rl_map) {
1367                 rl_map = key_alloc();
1368                 key_add(rl_map, "render-line:draw", &renderline_draw);
1369                 key_add(rl_map, "Refresh", &renderline_refresh);
1370                 key_add(rl_map, "render-line:measure", &renderline_measure);
1371                 key_add(rl_map, "render-line:findxy", &renderline_findxy);
1372                 key_add(rl_map, "get-attr", &renderline_get);
1373                 key_add(rl_map, "render-line:set", &renderline_set);
1374                 key_add(rl_map, "Close", &renderline_close);
1375                 key_add(rl_map, "Free", &edlib_do_free);
1376         }
1377
1378         p = pane_register(ci->focus, ci->num, &renderline_handle.c);
1379         if (!p)
1380                 return Efail;
1381         rd = &p->data;
1382         rd->line = strdup(ETX); // Imposible string
1383
1384         return comm_call(ci->comm2, "cb", p);
1385 }
1386
1387 void edlib_init(struct pane *ed safe)
1388 {
1389         call_comm("global-set-command", ed, &renderline_attach, 0, NULL,
1390                   "attach-renderline");
1391 }