@@@ dvdrip-upload: change settings while i'm stealing someone else's internet
[dvdrip] / multiprogress.c
1 /* -*-c-*-
2 *
3 * Progress bars for terminal programs
4 *
5 * (c) 2022 Mark Wooding
6 */
7
8 /*----- Licensing notice --------------------------------------------------*
9 *
10 * This library is free software: you can redistribute it and/or modify it
11 * under the terms of the GNU Lesser General Public License as published by
12 * the Free Software Foundation; either version 3 of the License, or (at your
13 * option) any later version.
14 *
15 * This library is distributed in the hope that it will be useful, but
16 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
17 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
18 * License for more details.
19 *
20 * You should have received a copy of the GNU Lesser General Public License
21 * along with this library. If not, see <https://www.gnu.org/licenses/>.
22 */
23
24 #define _XOPEN_SOURCE
25
26 /*----- Header files ------------------------------------------------------*/
27
28 #include <limits.h>
29 #include <stdarg.h>
30 #include <stdlib.h>
31 #include <string.h>
32 #include <wchar.h>
33
34 #include <unistd.h>
35 #include <termios.h>
36 #include <sys/ioctl.h>
37
38 #if defined(USE_TERMINFO)
39 # include <curses.h>
40 # include <term.h>
41 #elif defined(USE_TERMCAP)
42 # include <termcap.h>
43 #endif
44
45 #include "multiprogress.h"
46
47 /*----- Progress state lifecycle ------------------------------------------*/
48
49 static FILE *dup_stream(int fd)
50 /* Return a bidirectional `stdio' stream based on a duplicate of the
51 * file descriptor FD. (That way, we can safely `fclose' the copy
52 * without messing up the original descriptor, e.g., in the case
53 * where FD is standard output.)
54 */
55 {
56 FILE *fp;
57 int newfd;
58
59 newfd = dup(fd); if (newfd < 0) return (0);
60 fp = fdopen(newfd, "r+"); if (!fp) return (0);
61 return (fp);
62 }
63
64 int progress_init(struct progress_state *progress)
65 {
66 #ifdef USE_TERMCAP
67 char *term, *capcur;
68 #endif
69 #ifdef USE_TERMINFO
70 int err;
71 #endif
72 struct progress_ttyinfo *tty;
73 const char *t;
74 int n;
75
76 /* Clear the progress state. */
77 tty = &progress->tty;
78 tty->fp = 0;
79 tty->termbuf = tty->capbuf = 0;
80 tty->cap.f = 0;
81 tty->cap.cr = tty->cap.up = tty->cap.ce = tty->cap.cd =
82 tty->cap.mr = tty->cap.md = tty->cap.me =
83 tty->cap.af = tty->cap.ab = tty->cap.op = 0;
84
85 progress->items = progress->end_item = 0;
86 progress->nitems = 0; progress->last_lines = 0;
87 progress->tv_update.tv_sec = 0; progress->tv_update.tv_usec = 0;
88
89 /* Determine a suitable terminal. Use a copy of stdout or stderr if they
90 * look like terminals; otherwise, try to open `/dev/tty'. If nothing
91 * works, then give up.
92 */
93 if (isatty(1)) tty->fp = dup_stream(1);
94 else if (isatty(2)) tty->fp = dup_stream(2);
95 else tty->fp = fopen("/dev/tty", "r+");
96 if (!tty->fp) return (-1);
97
98 /* Determine the terminal width or height. DIM is `defwd' or `defht'
99 * accordingly; VAR names the environment variable which might have the
100 * relevant dimension; GETCAP is an expression to retrieve the default
101 * dimension from the capability database or return -1 if it's not there,
102 * and DFLT is a fallback default in case the database comes up short.
103 *
104 * Note that we try to use `TIOCGWINSZ' to get the size, so this is all
105 * part of an overly elaborate plan B.
106 */
107 #define SETDIM(dim, var, getcap, dflt) do { \
108 t = getenv(var); if (t) { n = atoi(t); if (n) { tty->dim = n; break; } } \
109 n = getcap; if (n > 0) { tty->dim = n; break; } \
110 tty->dim = dflt; \
111 } while (0)
112
113 /* Determine the terminal capabilities and size. */
114 #if defined(USE_TERMINFO)
115 /* Use `terminfo'. This is better than `termcap', but not by much. In
116 * particular, there's still a global current terminal-capabilities record
117 * which we'll clobber.
118 */
119
120 /* Look up the terminal. */
121 if (setupterm(0, fileno(tty->fp), &err) != OK || err < 1) return (-1);
122
123 /* Basic cursor motion and erasure. */
124 tty->cap.cr = tigetstr("cr");
125 tty->cap.nw = tigetstr("nel");
126 tty->cap.up = tigetstr("cuu1");
127 tty->cap.ce = tigetstr("el");
128 tty->cap.cd = tigetstr("ed");
129
130 if (tigetnum("xmc") < 1) {
131 /* No magic cookies, so check on the fancy highlighting. */
132
133 tty->cap.mr = tigetstr("rev");
134 tty->cap.md = tigetstr("bold");
135 tty->cap.me = tigetstr("sgr0");
136
137 tty->cap.af = tigetstr("setaf");
138 tty->cap.ab = tigetstr("setab");
139 tty->cap.op = tigetstr("op");
140 }
141
142 /* Find out whether erasure uses the current background colour. */
143 if (tigetflag("bce") > 0) tty->cap.f |= TCF_BCE;
144
145 /* Set the terminal size. */
146 SETDIM(defwd, "COLUMNS", tigetnum("co"), 80);
147 SETDIM(defht, "LINES", tigetnum("li"), 25);
148
149 #elif defined(USE_TERMCAP)
150 /* Use `termcap'. This is remarkably awful, really. We must guess at an
151 * upper bound on the size of a termcap string; memory is cheap now, so
152 * I've doubled the traditional size here. `tgetent' establishes a global
153 * current capability string, so we've clobbered that. And
154 */
155
156 /* Look up the terminal. */
157 tty->termbuf = malloc(4096); if (!tty->termbuf) return (-1);
158 tty->capbuf = malloc(4096); if (!tty->capbuf) return (-1);
159 term = getenv("TERM"); if (!term) return (-1);
160 if (tgetent(tty->termbuf, term) < 1) return (-1);
161 capcur = tty->capbuf;
162
163 /* Basic cursor motion and erasure. */
164 tty->cap.cr = tgetstr("cr", &capcur);
165 tty->cap.nw = tgetstr("nw", &capcur);
166 tty->cap.up = tgetstr("up", &capcur);
167 tty->cap.ce = tgetstr("ce", &capcur);
168 tty->cap.cd = tgetstr("cd", &capcur);
169
170 if (tgetnum("sg") < 1) {
171 /* No magic cookies, so check on the fancy highlighting. */
172
173 tty->cap.mr = tgetstr("mr", &capcur);
174 tty->cap.md = tgetstr("md", &capcur);
175 tty->cap.me = tgetstr("me", &capcur);
176
177 tty->cap.af = tgetstr("AF", &capcur);
178 tty->cap.ab = tgetstr("AB", &capcur);
179 tty->cap.op = tgetstr("op", &capcur);
180 }
181
182 /* Find out whether erasure uses the current background colour. */
183 if (tgetflag("ut") > 0) tty->cap.f |= TCF_BCE;
184
185 /* Set the pad character correctly. */
186 t = tgetstr("pc", &capcur); tty->cap.pc = t ? *t : 0;
187
188 /* Set the terminal size. */
189 SETDIM(defwd, "COLUMNS", tgetnum("co"), 80);
190 SETDIM(defht, "LINES", tgetnum("li"), 25);
191
192 #else
193 /* Nothing to do. Take a wild guess at the terminal size. */
194
195 SETDIM(defwd, "COLUMNS", -1, 80);
196 SETDIM(defht, "LINES", -1, 25);
197
198 #endif
199
200 #undef SETDIM
201
202 /* Fill in default motion. These are frequently omitted from capability
203 * strings.
204 */
205 if (!tty->cap.cr) tty->cap.cr = "\r";
206 if (!tty->cap.nw) tty->cap.nw = "\r\n";
207
208 /* If the terminal can't do the necessary motion and erasure then give up
209 * on it.
210 */
211 if (!tty->cap.up || !tty->cap.ce || !tty->cap.cd)
212 { fclose(tty->fp); tty->fp = 0; return (-1); }
213
214 /* If the terminal can't do all of the colour stuff we want, then clear
215 * `op' as a hint.
216 */
217 if (!tty->cap.af || !tty->cap.ab || !tty->cap.op) tty->cap.op = 0;
218
219 /* If the terminal can't return to normal, then clear bold and
220 * reverse-video.
221 */
222 if (!tty->cap.me) tty->cap.mr = tty->cap.md = 0;
223
224 /* Turn on full buffering. We take responsibility for forcing output at
225 * the right times.
226 */
227 setvbuf(tty->fp, 0, _IOFBF, BUFSIZ);
228
229 /* All done. */
230 return (0);
231 }
232
233 void progress_free(struct progress_state *progress)
234 {
235 struct progress_ttyinfo *tty = &progress->tty;
236
237 if (tty->fp) { fclose(tty->fp); tty->fp = 0; }
238 free(tty->termbuf); tty->termbuf = 0;
239 free(tty->capbuf); tty->capbuf = 0;
240 }
241
242 /*----- Active item list maintenance --------------------------------------*/
243
244 int progress_additem(struct progress_state *progress,
245 struct progress_item *item)
246 {
247 if (item->parent) return (-1);
248 item->prev = progress->end_item; item->next = 0;
249 if (progress->end_item) progress->end_item->next = item;
250 else progress->items = item;
251 progress->end_item = item; item->parent = progress;
252 progress->nitems++;
253
254 return (0);
255 }
256
257 int progress_removeitem(struct progress_state *progress,
258 struct progress_item *item)
259 {
260 if (!item->parent) return (-1);
261 if (item->next) item->next->prev = item->prev;
262 else (progress->end_item) = item->prev;
263 if (item->prev) item->prev->next = item->next;
264 else (progress->items) = item->next;
265 progress->nitems--; item->parent = 0;
266
267 return (0);
268 }
269
270 /*----- Render state lifecycle --------------------------------------------*/
271
272 static void setup_render_state(struct progress_state *progress,
273 struct progress_render_state *render)
274 {
275 const struct progress_ttyinfo *tty = &progress->tty;
276 struct winsize wsz;
277
278 /* Clear everything. */
279 render->tty = tty;
280 render->linebuf = 0; render->linesz = 0;
281 render->tempbuf = 0; render->tempsz = 0;
282
283 #ifdef USE_TERMCAP
284 /* Save old `termcap' globals. We'll restore them in `free_render_state'.
285 */
286 render->old_bc = BC; BC = 0;
287 render->old_up = UP; UP = 0;
288 render->old_pc = PC; PC = tty->cap.pc;
289 #endif
290
291 /* Determine the actual terminal size. Fall back on the default we
292 * established in `progress_init' if the kernel doesn't know.
293 */
294 if (!ioctl(fileno(tty->fp), TIOCGWINSZ, &wsz))
295 { render->width = wsz.ws_col; render->height = wsz.ws_row; }
296 else
297 { render->width = tty->defwd; render->height = tty->defht; }
298
299 /* We'll render progress bars with colour or standout if we can; otherwise,
300 * we'll just insert a `|' in the right place, but that takes up an extra
301 * column, so deduct one from the terminal's width to compensate.
302 */
303 if (render->width && !tty->cap.op && !tty->cap.mr) render->width--;
304 }
305
306 static void free_render_state(struct progress_render_state *render)
307 {
308 /* Send accumulated output to the terminal. */
309 fflush(render->tty->fp);
310
311 /* Free the buffers. */
312 free(render->linebuf); render->linebuf = 0; render->linesz = 0;
313 free(render->tempbuf); render->tempbuf = 0; render->tempsz = 0;
314
315 #ifdef USE_TERMCAP
316 /* Restore the `termcap' globals. */
317 UP = render->old_up;
318 BC = render->old_bc;
319 PC = render->old_pc;
320 #endif
321 }
322
323 /*----- Measuring string widths -------------------------------------------*/
324
325 #define CONV_MORE ((size_t)-2)
326 #define CONV_BAD ((size_t)-1)
327
328 struct measure {
329 mbstate_t ps; /* conversion state */
330 const char *p; size_t i, sz; /* input string, and cursor */
331 unsigned wd; /* width accumulated so far */
332 };
333
334 static void init_measure(struct measure *m, const char *p, size_t sz)
335 /* Set up M to measure the SZ-byte string P. */
336 {
337 m->p = p; m->sz = sz; m->i = 0; m->wd = 0;
338 memset(&m->ps, 0, sizeof(m->ps));
339 }
340
341 static int advance_measure(struct measure *m)
342 /* Advance the measurement in M by one character. Return zero if the
343 * end of the string has been reached, or nonzero if there is more to
344 * come.
345 */
346 {
347 wchar_t wch;
348 unsigned chwd;
349 size_t n;
350
351 /* Determine the next character's code WCH, the length N of its encoding in P
352 * in bytes, and the character's width CHWD in columns.
353 */
354 n = mbrtowc(&wch, m->p + m->i, m->sz - m->i, &m->ps);
355 if (!n) { chwd = 0; n = m->sz - m->i; }
356 else if (n == CONV_MORE) { chwd = 2; n = m->sz - m->i; }
357 else if (n == CONV_BAD) { chwd = 2; n = 1; }
358 else chwd = wcwidth(wch);
359
360 /* Advance the state. */
361 m->i += n; m->wd += chwd;
362
363 /* Report whether there's more to come. */
364 return (m->i < m->sz);
365 }
366
367 static unsigned string_width(const char *p, size_t sz)
368 /* Return the width of the SZ-byte string P, in terminal columns. */
369 {
370 struct measure m;
371
372 init_measure(&m, p, sz);
373 while (advance_measure(&m));
374 return (m.wd);
375 }
376
377 static size_t split_string(const char *p, size_t sz,
378 unsigned *wd_out, unsigned maxwd)
379 /* Return the size, in bytes, of the shortest prefix of the SZ-byte
380 * string P which is no less than MAXWD columns wide, or SZ if it's
381 * just too short. Store the actual width in *WD_OUT.
382 */
383 {
384 struct measure m;
385 size_t i; unsigned wd;
386 int more;
387
388 init_measure(&m, p, sz);
389
390 /* Advance until we're past the bound. */
391 for (;;) {
392 if (!advance_measure(&m)) { *wd_out = m.wd; return (sz); }
393 if (m.wd >= maxwd) break;
394 }
395
396 /* Now /continue/ advancing past zero-width characters until we find
397 * something that wasn't zero-width. These might be combining accents or
398 * somesuch, and leaving them off would definitely be wrong.
399 */
400 wd = m.wd; i = m.i;
401 for (;;) {
402 more = advance_measure(&m);
403 if (m.wd > wd) break;
404 i = m.i;
405 if (!more) break;
406 }
407
408 /* All done. */
409 *wd_out = wd; return (i);
410 }
411
412 /*----- Output buffer handling --------------------------------------------*/
413
414 static int grow_linebuf(struct progress_render_state *render, size_t want)
415 /* Extend the line buffer in RENDER so that it's at least WANT bytes
416 * long. Shuffle the accumulated left and right material in the
417 * buffer as necessary. Return 0 on success or -1 if this fails for
418 * any reason.
419 */
420 {
421 char *newbuf; size_t newsz;
422
423 /* Return if there's already enough space. */
424 if (want <= render->linesz) return (0);
425
426 /* Work out how much space to allocate. The initial size is a rough guess
427 * based on the size of UTF-8 encoded characters, though it's not an upper
428 * bound because many characters have zero width. Double the buffer size
429 * if it's too small. Sneakily insert a terminating zero byte just in
430 * case.
431 */
432 if (!render->linesz) newsz = 4*render->width + 1;
433 else newsz = render->linesz;
434 while (newsz < want) newsz *= 2;
435 newbuf = malloc(newsz + 1); if (!newbuf) return (-1);
436 newbuf[newsz] = 0;
437
438 /* Copy the left and right strings into the new buffer. */
439 if (render->leftsz)
440 memcpy(newbuf, render->linebuf, render->leftsz);
441 if (render->rightsz)
442 memcpy(newbuf + newsz - render->rightsz,
443 render->linebuf + render->linesz - render->rightsz,
444 render->rightsz);
445
446 /* Free the old buffer and remember the new one. */
447 free(render->linebuf); render->linebuf = newbuf; render->linesz = newsz;
448
449 /* All done. */
450 return (0);
451 }
452
453 static int grow_tempbuf(struct progress_render_state *render, size_t want)
454 /* Extend the temporary buffer in RENDER so that it's at least WANT
455 * bytes long. Anything stored in the buffer will be lost. Return 0
456 * on success or -1 if this fails for any reason.
457 */
458 {
459 char *newbuf; size_t newsz;
460
461 /* Return if there's already enough space. */
462 if (want <= render->tempsz) return (0);
463
464 /* Work out how much space to allocate. This is the same as `grow_linebuf'
465 * above.
466 */
467 if (!render->tempsz) newsz = 4*render->width + 1;
468 else newsz = render->tempsz;
469 while (newsz < want) newsz *= 2;
470 newbuf = malloc(newsz + 1); if (!newbuf) return (-1);
471 newbuf[newsz] = 0;
472
473 /* Free the old buffer and keep the new one. */
474 free(render->tempbuf); render->tempbuf = newbuf; render->tempsz = newsz;
475
476 /* All done. */
477 return (0);
478 }
479
480 enum { LEFT, RIGHT };
481 static int putstr(struct progress_render_state *render, unsigned side,
482 const char *p, size_t n)
483 /* Add the N-byte string P to SIDE of the line buffer in RENDER.
484 * Return 0 on success or -1 if this fails for any reason.
485 */
486 {
487 unsigned newwd = string_width(p, n);
488 size_t want;
489
490 if (newwd >= render->width - render->leftwd - render->rightwd) return (-1);
491 want = render->leftsz + render->rightsz + n;
492 if (want > render->linesz && grow_linebuf(render, want)) return (-1);
493 switch (side) {
494 case LEFT:
495 memcpy(render->linebuf + render->leftsz, p, n);
496 render->leftsz += n; render->leftwd += newwd;
497 break;
498 case RIGHT:
499 memcpy(render->linebuf + render->linesz - render->rightsz - n, p, n);
500 render->rightsz += n; render->rightwd += newwd;
501 break;
502 default:
503 return (-1);
504 }
505 return (0);
506 }
507
508 static int vputf(struct progress_render_state *render, unsigned side,
509 const char *fmt, va_list ap)
510 /* Format a `printf'-style string FMT with arguments AP, and add it
511 * to SIDE of RENDER's line buffer.
512 */
513 {
514 va_list bp;
515 int rc;
516
517 if (!render->tempsz && grow_tempbuf(render, 2*strlen(fmt))) return (-1);
518 for (;;) {
519 va_copy(bp, ap);
520 rc = vsnprintf(render->tempbuf, render->tempsz, fmt, bp);
521 va_end(bp);
522 if (rc < 0) return (-1);
523 if (rc <= render->tempsz) break;
524 if (grow_tempbuf(render, 2*(rc + 1))) return (-1);
525 }
526 if (putstr(render, side, render->tempbuf, rc)) return (-1);
527 return (0);
528 }
529
530 int progress_vputleft(struct progress_render_state *render,
531 const char *fmt, va_list ap)
532 { return (vputf(render, LEFT, fmt, ap)); }
533
534 int progress_vputright(struct progress_render_state *render,
535 const char *fmt, va_list ap)
536 { return (vputf(render, RIGHT, fmt, ap)); }
537
538 int progress_putleft(struct progress_render_state *render,
539 const char *fmt, ...)
540 {
541 va_list ap;
542 int rc;
543
544 va_start(ap, fmt); rc = vputf(render, LEFT, fmt, ap); va_end(ap);
545 return (rc);
546 }
547
548 int progress_putright(struct progress_render_state *render,
549 const char *fmt, ...)
550 {
551 va_list ap;
552 int rc;
553
554 va_start(ap, fmt); rc = vputf(render, RIGHT, fmt, ap); va_end(ap);
555 return (rc);
556 }
557
558 /*----- Terminal output ---------------------------------------------------*/
559
560 #if defined(USE_TERMINFO)
561
562 static const struct progress_ttyinfo *curtty = 0;
563 static int putty(int ch) { return (putc(ch, curtty->fp)); }
564 void progress_put_sequence(const struct progress_ttyinfo *tty,
565 const char *p, unsigned nlines)
566 { if (p) { curtty = tty; tputs(p, nlines, putty); } }
567 void progress_set_fgcolour(const struct progress_ttyinfo *tty, int colour)
568 { progress_put_sequence(tty, tgoto(tty->cap.af, -1, colour), 1); }
569 void progress_set_bgcolour(const struct progress_ttyinfo *tty, int colour)
570 { progress_put_sequence(tty, tgoto(tty->cap.ab, -1, colour), 1); }
571
572 #elif defined(USE_TERMCAP)
573
574 static const struct progress_ttyinfo *curtty = 0;
575 static int putty(int ch) { return (putc(ch, curtty->fp)); }
576 void progress_put_sequence(const struct progress_ttyinfo *tty,
577 const char *p, unsigned nlines)
578 { if (p) { curtty = tty; tputs(p, nlines, putty); } }
579 void progress_set_fgcolour(const struct progress_ttyinfo *tty, int colour)
580 { progress_put_sequence(tty, tgoto(tty->cap.af, -1, colour), 1); }
581 void progress_set_bgcolour(const struct progress_ttyinfo *tty, int colour)
582 { progress_put_sequence(tty, tgoto(tty->cap.ab, -1, colour), 1); }
583
584 #else
585
586 void progress_put_sequence(const struct progress_ttyinfo *tty,
587 const char *p, unsigned nlines) { ; }
588 void progress_set_fgcolour(const struct progress_ttyinfo *tty, int colour)
589 { ; }
590 void progress_set_bgcolour(const struct progress_ttyinfo *tty, int colour)
591 { ; }
592
593 #endif
594
595 /*----- Maintaining the progress display ----------------------------------*/
596
597 #define CLRF_ALL 1u /* clear everything */
598 static void clear_progress(struct progress_state *progress,
599 struct progress_render_state *render, unsigned f)
600 /* Clear the current progress display maintained by PROGRESS,
601 * assisted by the RENDER state.
602 *
603 * If `CLRF_ALL' is set in F, then clear the entire display.
604 * Otherwise, clear the bottom few lines if there are now fewer
605 * progress items than there were last time we rendered the display,
606 * and leave the cursor at the start of the top line ready to
607 * overwrite it.
608 */
609 {
610 const struct progress_ttyinfo *tty = &progress->tty;
611 unsigned ndel, nleave;
612 unsigned i;
613
614 progress_put_sequence(tty, tty->cap.cr, 1);
615 if (progress->last_lines) {
616
617 /* Decide how many lines to delete. Set `ndel' to the number of lines
618 * that will be entirely erased, and `nleave' to the number that we'll
619 * leave.
620 */
621 if (f&CLRF_ALL)
622 { ndel = progress->last_lines; nleave = 0; }
623 else {
624 if (progress->nitems >= progress->last_lines) ndel = 0;
625 else ndel = progress->last_lines - progress->nitems;
626 nleave = progress->last_lines - ndel;
627 }
628
629 /* Now actually do the clearing. Remember that the cursor is still on
630 * the last line.
631 */
632 if (!ndel)
633 for (i = 1; i < nleave; i++)
634 progress_put_sequence(tty, tty->cap.up, 1);
635 else {
636 for (i = 1; i < ndel; i++)
637 progress_put_sequence(tty, tty->cap.up, 1);
638 progress_put_sequence(tty, tty->cap.cd, ndel);
639 for (i = 0; i < nleave; i++)
640 progress_put_sequence(tty, tty->cap.up, 1);
641 }
642 }
643
644 /* Remember that we're now at the top of the display. */
645 progress->last_lines = 0;
646 }
647
648 int progress_clear(struct progress_state *progress)
649 {
650 struct progress_render_state render;
651
652 if (!progress->tty.fp) return (-1);
653 setup_render_state(progress, &render);
654 clear_progress(progress, &render, CLRF_ALL);
655 free_render_state(&render);
656 return (0);
657 }
658
659 int progress_update(struct progress_state *progress)
660 {
661 struct progress_render_state render;
662 const struct progress_ttyinfo *tty = &progress->tty;
663 struct progress_item *item;
664 unsigned f = 0;
665 #define f_any 1u
666
667 if (!progress->tty.fp) return (-1);
668
669 setup_render_state(progress, &render);
670 clear_progress(progress, &render, 0);
671
672 for (item = progress->items; item; item = item->next) {
673 if (f&f_any) progress_put_sequence(tty, tty->cap.nw, 1);
674 render.leftsz = render.rightsz = 0;
675 render.leftwd = render.rightwd = 0;
676 item->render(item, &render); progress->last_lines++; f |= f_any;
677 if (progress->last_lines > render.height) break;
678 }
679 if (f&f_any) progress_put_sequence(tty, tty->cap.cr, 1);
680 free_render_state(&render);
681 return (0);
682 }
683
684 /*----- Rendering progress bars -------------------------------------------*/
685
686 /* The basic problem here is to render text, formed of several pieces, to the
687 * terminal, placing some marker in the middle of it to indicate how much
688 * progress has been made. This marker might be a colour change, switching
689 * off reverse-video mode, or a `|' character.
690 */
691
692 enum {
693 LEFT_COLOUR,
694 LEFT_MONO,
695 LEFT_SIMPLE,
696 RIGHT_ANY,
697 STOP
698 };
699
700 struct bar_state {
701 /* State to track progress through the output of a progress bar, so
702 * that we insert the marker in the right place.
703 *
704 * This is a little state machine. We remember the current column
705 * position, the current state, and the column at which we'll next
706 * change state.
707 */
708
709 const struct progress_render_state *render; /* render state */
710 unsigned pos, nextpos, state; /* as described */
711 };
712
713 static void advance_bar_state(struct bar_state *bar)
714 /* If we've reached the column position for the next state change
715 * then arrange to do whatever it is we're meant to do, and update
716 * for the next change.
717 */
718 {
719 const struct progress_render_state *render = bar->render;
720 const struct progress_ttyinfo *tty = render->tty;
721 size_t here = bar->nextpos;
722
723 while (bar->nextpos <= here) {
724 switch (bar->state) {
725 case LEFT_COLOUR: progress_set_bgcolour(tty, 3); goto right;
726 case LEFT_MONO: progress_put_sequence(tty, tty->cap.me, 1); goto right;
727 case LEFT_SIMPLE: putc('|', tty->fp); goto right;
728 right: bar->state = RIGHT_ANY; bar->nextpos = render->width; break;
729 case RIGHT_ANY: bar->state = STOP; bar->nextpos = UINT_MAX; break;
730 }
731 }
732 }
733
734 /* Little utilities to output a chunk of text, or some spaces. */
735 static void put_str(FILE *fp, const char *p, size_t sz)
736 { while (sz--) putc(*p++, fp); }
737 static void put_spc(FILE *fp, unsigned n)
738 { while (n--) putc(' ', fp); }
739
740 static void put_barstr(struct bar_state *bar, const char *p, size_t sz)
741 /* Output the SZ-byte string P, driving the state machine BAR as we
742 * go.
743 */
744 {
745 unsigned wd;
746 size_t n;
747
748 for (;;) {
749 /* Main loop. Determine how much space there is to the next state
750 * change, cut off that amount of space from the string, and advance.
751 */
752
753 n = split_string(p, sz, &wd, bar->nextpos - bar->pos);
754 if (n == sz && wd < bar->nextpos - bar->pos) break;
755 put_str(bar->render->tty->fp, p, n); bar->pos += wd;
756 advance_bar_state(bar);
757 p += n; sz -= n;
758 }
759
760 /* Write out the rest of the string, and update the position. We know that
761 * this won't reach the next transition.
762 */
763 put_str(bar->render->tty->fp, p, sz); bar->pos += wd;
764 }
765
766 static void put_barspc(struct bar_state *bar, unsigned n)
767 /* Output N spaces, driving the state machine BAR as we go. */
768 {
769 unsigned step;
770
771 for (;;) {
772 step = bar->nextpos - bar->pos;
773 if (n < step) break;
774 put_spc(bar->render->tty->fp, step); bar->pos += step;
775 advance_bar_state(bar);
776 n -= step;
777 }
778 put_spc(bar->render->tty->fp, n); bar->pos += n;
779 }
780
781 int progress_showbar(struct progress_render_state *render, double frac)
782 {
783 const struct progress_ttyinfo *tty = render->tty;
784 struct bar_state bar;
785
786 /* If there's no terminal, then there's nothing to do. */
787 if (!tty->fp) return (-1);
788
789 /* Set up the render state, with a transition where the bar should end. */
790 bar.render = render; bar.pos = 0; bar.nextpos = frac*render->width + 0.5;
791
792 /* Set the initial state for the render. */
793 if (tty->cap.op) {
794 /* We have colour. The foreground will always be black. If we've made
795 * negligible progress then advance the state machine immediately, which
796 * will set a yellow background for the remainder of the line; otherwise
797 * set the background green for the start of the bar.
798
799 */
800 progress_set_fgcolour(tty, 0); bar.state = LEFT_COLOUR;
801 if (bar.nextpos) progress_set_bgcolour(tty, 2);
802 else advance_bar_state(&bar);
803 } else if (tty->cap.mr) {
804 /* We have reverse-video. Write the progress bar in reverse and the rest
805 * in normal.
806 */
807
808 if (bar.nextpos)
809 { bar.state = LEFT_MONO; progress_put_sequence(tty, tty->cap.mr, 1); }
810 else
811 { bar.state = RIGHT; bar.nextpos = render->width; }
812 } else
813 /* Nothing fancy. We'll write `|' at the right place. */
814 bar.state = LEFT_SIMPLE;
815
816 /* Write the left string, spaces to fill the gap, and the right string. */
817 put_barstr(&bar, render->linebuf, render->leftsz);
818 put_barspc(&bar, render->width - render->leftwd - render->rightwd);
819 put_barstr(&bar,
820 render->linebuf + render->linesz - render->rightsz,
821 render->rightsz);
822
823 /* Final output: turn off fancy highlighting, and colours. */
824 progress_put_sequence(tty, tty->cap.me, 1);
825 progress_put_sequence(tty, tty->cap.op, 1);
826
827 /* All done. */
828 return (0);
829 }
830
831 int progress_shownotice(struct progress_render_state *render, int bg, int fg)
832 {
833 const struct progress_ttyinfo *tty = render->tty;
834
835 /* If there's no terminal, then there's nothing to do. */
836 if (!tty->fp) return (-1);
837
838 /* Set the general background for the notice. */
839 if (tty->cap.op) {
840 /* We have colours, so set them. */
841
842 progress_set_fgcolour(tty, fg); progress_set_bgcolour(tty, bg);
843 } else if (tty->cap.mr) {
844 /* We have reverse-video, so we might as well use that. */
845
846 progress_put_sequence(tty, tty->cap.mr, 1);
847 }
848
849 /* Set boldface. (If we have it.) */
850 progress_put_sequence(tty, tty->cap.md, 1);
851
852 /* Print the left string. If there's a right string, then print spaces and
853 * the right string; otherwise, try to optimize by erasing to the end of
854 * the line -- if that will erase in the background colour.
855 */
856 put_str(tty->fp, render->linebuf, render->leftsz);
857 if (!render->rightsz && (tty->cap.f&TCF_BCE) && tty->cap.ce)
858 progress_put_sequence(tty, tty->cap.ce, 1);
859 else {
860 put_spc(tty->fp, render->width - render->leftwd - render->rightwd);
861 put_str(tty->fp,
862 render->linebuf + render->linesz - render->rightsz,
863 render->rightsz);
864 }
865
866 /* Put things back to normal. */
867 progress_put_sequence(tty, tty->cap.me, 1);
868 progress_put_sequence(tty, tty->cap.op, 1);
869
870 /* All done. */
871 return (0);
872 }
873
874 /*----- That's all, folks -------------------------------------------------*/