3 * Progress bars for terminal programs
5 * (c) 2025 Straylight/Edgeware
8 /*----- Licensing notice --------------------------------------------------*
10 * This file is part of the mLib utilities library.
12 * mLib is free software: you can redistribute it and/or modify it under
13 * the terms of the GNU Library General Public License as published by
14 * the Free Software Foundation; either version 2 of the License, or (at
15 * your option) any later version.
17 * mLib is distributed in the hope that it will be useful, but WITHOUT
18 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
19 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
20 * License for more details.
22 * You should have received a copy of the GNU Library General Public
23 * License along with mLib. If not, write to the Free Software
24 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
28 /*----- Header files ------------------------------------------------------*/
46 #include "ttycolour.h"
47 #include "ttyprogress.h"
49 /*----- Main code ---------------------------------------------------------*/
51 int ttyprogress_init(struct ttyprogress *progress, struct tty *tty)
53 #define GENATTR(want, attrs, fgspc, fgcol, bgspc, bgcol) \
55 { ((fgspc) << TTAF_FGSPCSHIFT) | \
56 ((bgspc) << TTAF_BGSPCSHIFT) | (attrs), \
57 0, (fgcol), (bgcol) } }
59 #define FGBG(want, attrs, fgcol, bgcol) \
60 GENATTR(want | TTACF_FG | TTACF_BG, attrs, \
61 TTCSPC_1BPCBR, TTCOL_##fgcol, TTCSPC_1BPCBR, TTCOL_##bgcol)
62 #define ATTR(want, attrs) \
63 GENATTR(want, attrs, TTCSPC_NONE, 0, TTCSPC_NONE, 0)
65 #define BOLD (TTWT_BOLD << TTAF_WTSHIFT)
67 static const struct tty_attrlist
68 bbar_attrs[] = { FGBG(0, 0, BLK, GRN), ATTR(0, TTAF_INVV) },
69 ebar_attrs[] = { FGBG(0, 0, BLK, YLW), TTY_ATTRLIST_CLEAR },
70 note_attrs[] = { FGBG(0, BOLD, WHT, BLU), ATTR(0, TTAF_INVV | BOLD) },
71 warn_attrs[] = { FGBG(0, BOLD, WHT, MGN), ATTR(0, TTAF_INVV | BOLD) },
72 err_attrs[] = { FGBG(0, BOLD, WHT, RED), ATTR(0, TTAF_INVV | BOLD) };
73 static const struct ttycolour_style hltab[] =
74 TTYCOLOUR_INITTAB(TTYPROGRESS_HIGHLIGHTS);
81 /* Clear the progress state. */
83 progress->items = progress->end_item = 0;
84 progress->nitems = 0; progress->last_lines = 0;
85 progress->line.a = arena_global; dstr_create(&progress->line.t);
86 progress->line.p = 0; progress->line.sz = 0;
87 progress->tv_update.tv_sec = 0; progress->tv_update.tv_usec = 0;
89 /* Check that the terminal is sufficiently cromulent. */
90 if (!tty || !(tty->ocaps&TTCF_RELMV) || !(tty->ocaps&TTCF_EREOD))
91 { progress->tty = 0; return (-1); }
93 /* Configure the highlight attributes. */
94 ttycolour_config(progress->attr,
95 "MLIB_TTYPROGRESS_COLOURS", TCIF_GETENV, tty, hltab);
101 void ttyprogress_free(struct ttyprogress *progress)
103 dstr_destroy(&progress->line.t);
104 x_free(progress->line.a, progress->line.p);
107 /*----- Active item list maintenance --------------------------------------*/
109 int ttyprogress_additem(struct ttyprogress *progress,
110 struct ttyprogress_item *item)
112 if (item->parent) return (-1);
113 item->prev = progress->end_item; item->next = 0;
114 if (progress->end_item) progress->end_item->next = item;
115 else progress->items = item;
116 progress->end_item = item; item->parent = progress;
122 int ttyprogress_removeitem(struct ttyprogress *progress,
123 struct ttyprogress_item *item)
125 if (!item->parent) return (-1);
126 if (item->next) item->next->prev = item->prev;
127 else (progress->end_item) = item->prev;
128 if (item->prev) item->prev->next = item->next;
129 else (progress->items) = item->next;
130 progress->nitems--; item->parent = 0;
135 /*----- Render state lifecycle --------------------------------------------*/
137 static void setup_render_state(struct ttyprogress *progress,
138 struct ttyprogress_render *render)
140 struct tty *tty = progress->tty;
142 /* Clear everything. */
145 /* Update the current terminal size. */
148 /* We'll render progress bars with colour or standout if we can; otherwise,
149 * we'll just insert a `|' in the right place, but that takes up an extra
150 * column, so deduct one from the terminal's width to compensate. Deduct
151 * another one if the terminal doesn't let us leave the cursor in the final
152 * column without scrolling or wrapping.
154 render->width = tty->wd;
155 if (render->width && !(tty->acaps&(TTCF_MMARG | TTMF_AUTOM)))
157 if (render->width && !(tty->acaps&(TTACF_BG | TTACF_INVV)))
160 /* Borrow the line buffer and highlight table from the master state. */
161 render->line = &progress->line;
162 render->attr = progress->attr;
165 /*----- Measuring string widths -------------------------------------------*/
167 #if defined(HAVE_MBRTOWC) && defined(HAVE_WCWIDTH)
171 #define CONV_MORE ((size_t)-2)
172 #define CONV_BAD ((size_t)-1)
175 mbstate_t ps; /* conversion state */
176 const char *p; size_t i, sz; /* input string, and cursor */
177 unsigned wd; /* width accumulated so far */
180 static void init_measure(struct measure *m, const char *p, size_t sz)
181 /* Set up M to measure the SZ-byte string P. */
183 m->p = p; m->sz = sz; m->i = 0; m->wd = 0;
184 memset(&m->ps, 0, sizeof(m->ps));
187 static int advance_measure(struct measure *m)
188 /* Advance the measurement in M by one character. Return zero if the
189 * end of the string has been reached, or nonzero if there is more to
197 /* Determine the next character's code WCH, the length N of its encoding in
198 * P in bytes, and the character's width CHWD in columns.
200 n = mbrtowc(&wch, m->p + m->i, m->sz - m->i, &m->ps);
201 if (!n) { chwd = 0; n = m->sz - m->i; }
202 else if (n == CONV_MORE) { chwd = 2; n = m->sz - m->i; }
203 else if (n == CONV_BAD) { chwd = 2; n = 1; }
204 else chwd = wcwidth(wch);
206 /* Advance the state. */
207 m->i += n; m->wd += chwd;
209 /* Report whether there's more to come. */
210 return (m->i < m->sz);
213 static unsigned string_width(const char *p, size_t sz)
214 /* Return the width of the SZ-byte string P, in terminal columns. */
218 init_measure(&m, p, sz);
219 while (advance_measure(&m));
223 static size_t split_string(const char *p, size_t sz,
224 unsigned *wd_out, unsigned maxwd)
225 /* Return the size, in bytes, of the shortest prefix of the SZ-byte
226 * string P which is no less than MAXWD columns wide, or SZ if it's
227 * just too short. Store the actual width in *WD_OUT.
231 size_t i; unsigned wd;
234 init_measure(&m, p, sz);
236 /* Advance until we're past the bound. */
238 if (!advance_measure(&m)) { *wd_out = m.wd; return (sz); }
239 if (m.wd >= maxwd) break;
242 /* Now /continue/ advancing past zero-width characters until we find
243 * something that wasn't zero-width. These might be combining accents or
244 * somesuch, and leaving them off would definitely be wrong.
248 more = advance_measure(&m);
249 if (m.wd > wd) break;
255 *wd_out = wd; return (i);
260 static unsigned string_width(const char *p, size_t sz) { return (sz); }
262 static size_t split_string(const char *p, size_t sz,
263 unsigned *wd_out, unsigned maxwd)
267 if (sz <= maxwd) wd = sz;
269 *wd_out = wd; return (wd);
274 /*----- Output buffer handling --------------------------------------------*/
276 static void grow_linebuf(struct ttyprogress_render *render, size_t want)
277 /* Extend the line buffer in RENDER so that it's at least WANT bytes
278 * long. Shuffle the accumulated left and right material in the
279 * buffer as necessary.
282 struct ttyprogress_buffer *line = render->line;
283 char *newbuf; size_t newsz;
285 /* Return if there's already enough space. */
286 if (want <= line->sz) return;
288 /* Work out how much space to allocate. The initial size is a rough guess
289 * based on the size of UTF-8 encoded characters, though it's not an upper
290 * bound because many characters have zero width. Double the buffer size
291 * if it's too small. Sneakily insert a terminating zero byte just in
295 GROWBUF_SIZE(size_t, newsz, want, 4*render->width + 1, 1);
296 newbuf = x_alloc(line->a, newsz + 1);
299 /* Copy the left and right strings into the new buffer. */
301 memcpy(newbuf, line->p, render->leftsz);
303 memcpy(newbuf + newsz - render->rightsz,
304 line->p + line->sz - render->rightsz,
307 /* Free the old buffer and remember the new one. */
308 x_free(line->a, line->p); line->p = newbuf; line->sz = newsz;
311 enum { LEFT, RIGHT, STOP };
313 static int putstr(struct ttyprogress_render *render, unsigned side,
314 const char *p, size_t n)
315 /* Add the N-byte string P to SIDE of the line buffer in RENDER.
316 * Return 0 on success or -1 if this fails for any reason.
319 unsigned newwd = string_width(p, n);
322 if (newwd >= render->width - render->leftwd - render->rightwd) return (-1);
323 want = render->leftsz + render->rightsz + n;
324 if (want > render->line->sz) grow_linebuf(render, want);
327 memcpy(render->line->p + render->leftsz, p, n);
328 render->leftsz += n; render->leftwd += newwd;
331 memcpy(render->line->p + render->line->sz - render->rightsz - n, p, n);
332 render->rightsz += n; render->rightwd += newwd;
340 int ttyprogress_vputleft(struct ttyprogress_render *render,
341 const char *fmt, va_list *ap)
343 dstr *t = &render->line->t;
345 DRESET(t); dstr_vputf(t, fmt, ap);
346 return (putstr(render, LEFT, t->buf, t->len));
349 int ttyprogress_vputright(struct ttyprogress_render *render,
350 const char *fmt, va_list *ap)
352 dstr *t = &render->line->t;
354 DRESET(t); dstr_vputf(t, fmt, ap);
355 return (putstr(render, RIGHT, t->buf, t->len));
358 int ttyprogress_putleft(struct ttyprogress_render *render,
359 const char *fmt, ...)
364 va_start(ap, fmt); rc = ttyprogress_vputleft(render, fmt, &ap); va_end(ap);
368 int ttyprogress_putright(struct ttyprogress_render *render,
369 const char *fmt, ...)
374 va_start(ap, fmt); rc = ttyprogress_vputright(render, fmt, &ap); va_end(ap);
378 /*----- Maintaining the progress display ----------------------------------*/
380 #define CLRF_ALL 1u /* clear everything */
381 static void clear_progress(struct ttyprogress *progress, unsigned f)
382 /* Clear the current progress display maintained by PROGRESS.
384 * If `CLRF_ALL' is set in F, then clear the entire display.
385 * Otherwise, clear the bottom few lines if there are now fewer
386 * progress items than there were last time we rendered the display,
387 * and leave the cursor at the start of the top line ready to
391 struct tty *tty = progress->tty;
394 if (progress->last_lines) {
396 /* Decide how many lines to delete. Set `ndel' to the number of lines
397 * that will be entirely erased, and `nleave' to the number that we'll
401 { ndel = progress->last_lines; nleave = 0; }
403 if (progress->nitems >= progress->last_lines) ndel = 0;
404 else ndel = progress->last_lines - progress->nitems;
405 nleave = progress->last_lines - ndel;
408 /* Now actually do the clearing. Remember that the cursor is still on
412 tty_move(tty, TTOF_YCUR | TTOF_XHOME, 1 - nleave, 0);
414 tty_move(tty, TTOF_YCUR | TTOF_XHOME, 1 - ndel, 0);
415 tty_erase(tty, TTEF_DSP | TTEF_END);
416 tty_move(tty, TTORG_CUR, -nleave, 0);
420 /* Remember that we're now at the top of the display. */
421 progress->last_lines = 0;
424 int ttyprogress_clear(struct ttyprogress *progress)
426 if (!progress->tty) return (-1);
427 clear_progress(progress, CLRF_ALL);
431 int ttyprogress_update(struct ttyprogress *progress)
433 struct ttyprogress_render render;
434 struct ttyprogress_item *item;
435 struct tty *tty = progress->tty;
436 struct tty_attr save;
441 if (!tty) return (-1);
443 setup_render_state(progress, &render);
444 clear_progress(progress, 0);
445 modes = tty->st.modes; tty_setmodes(tty, TTMF_AUTOM, 0);
448 for (item = progress->items; item; item = item->next) {
449 if (f&f_any) tty_move(tty, TTOF_YCUR | TTOF_XHOME, 1, 0);
450 render.leftsz = render.rightsz = 0;
451 render.leftwd = render.rightwd = 0;
452 item->render(item, &render); progress->last_lines++; f |= f_any;
453 if (progress->last_lines > tty->ht) break;
455 tty_setmodes(tty, MASK32, modes); tty_setattr(tty, &save);
462 /*----- Rendering progress bars -------------------------------------------*/
464 /* The basic problem here is to render text, formed of several pieces, to the
465 * terminal, placing some marker in the middle of it to indicate how much
466 * progress has been made. This marker might be a colour change, switching
467 * off reverse-video mode, or a `|' character.
471 /* State to track progress through the output of a progress bar, so
472 * that we insert the marker in the right place.
474 * This is a little state machine. We remember the current column
475 * position, the current state, and the column at which we'll next
479 const struct ttyprogress_render *render; /* render state */
480 unsigned pos, nextpos, state; /* as described */
483 static void advance_bar_state(struct bar *bar)
484 /* If we've reached the column position for the next state change
485 * then arrange to do whatever it is we're meant to do, and update
486 * for the next change.
489 const struct ttyprogress_render *render = bar->render;
490 struct tty *tty = render->tty;
491 size_t here = bar->nextpos;
493 while (bar->nextpos <= here) {
494 switch (bar->state) {
496 if (!(tty->acaps&(TTACF_BG | TTACF_INVV))) putc('|', tty->fpout);
497 else tty_setattr(tty, &render->attr[TPHL_EBAR]);
498 bar->state = RIGHT; bar->nextpos = render->width;
501 bar->state = STOP; bar->nextpos = UINT_MAX;
507 /* Little utility to output a chunk of text. */
508 static void put_str(FILE *fp, const char *p, size_t sz)
509 { while (sz--) putc(*p++, fp); }
511 static void put_spc(struct tty *tty, unsigned n)
513 if (!(~tty->ocaps&(TTCF_RELMV | TTCF_BGER | TTCF_ERCH)))
514 { tty_erch(tty, n); tty_move(tty, TTORG_CUR, 0, n); }
516 tty_repeat(tty, ' ', n);
519 static void put_barstr(struct bar *bar, const char *p, size_t sz)
520 /* Output the SZ-byte string P, driving the state machine BAR as we
524 FILE *fp = bar->render->tty->fpout;
529 /* Main loop. Determine how much space there is to the next state
530 * change, cut off that amount of space from the string, and advance.
533 n = split_string(p, sz, &wd, bar->nextpos - bar->pos);
534 if (n == sz && wd < bar->nextpos - bar->pos) break;
535 put_str(fp, p, n); bar->pos += wd; advance_bar_state(bar);
539 /* Write out the rest of the string, and update the position. We know that
540 * this won't reach the next transition.
542 put_str(fp, p, sz); bar->pos += wd;
545 static void put_barspc(struct bar *bar, unsigned n)
546 /* Output N spaces, driving the state machine BAR as we go. */
548 struct tty *tty = bar->render->tty;
552 step = bar->nextpos - bar->pos;
554 put_spc(tty, step); bar->pos += step; n -= step;
555 advance_bar_state(bar);
557 put_spc(tty, n); bar->pos += n;
560 int ttyprogress_showbar(struct ttyprogress_render *render, double frac)
562 struct tty *tty = render->tty;
563 const struct ttyprogress_buffer *line = render->line;
566 /* If there's no terminal, then there's nothing to do. */
567 if (!tty) return (-1);
569 /* Clamp the fraction. Rounding errors in the caller's calculation might
570 * legitimately leave it slightly out of bounds. The sense of the
571 * comparisons also catches QNaNs, which are silently squashed to zero so
572 * as to prevent anything untoward happening when we convert to integer
575 frac = !(frac >= 0.0) ? 0.0 : !(frac <= 1.0) ? 1.0 : frac;
577 /* Set up the render state, with a transition where the bar should end. */
578 bar.render = render; bar.pos = 0; bar.nextpos = frac*render->width + 0.5;
580 /* Set the initial state for the render. */
581 if (tty->acaps&(TTACF_BG | TTACF_INVV)) {
582 /* We have highlighting. If we have made only negligible progress then
583 * advance the state machine immediately, which will set the correct
584 * highlighting; otherwise, set the beginning-of-bar highlight.
588 if (bar.nextpos) tty_setattr(tty, &render->attr[TPHL_BBAR]);
589 else advance_bar_state(&bar);
591 /* Nothing fancy. We'll write `|' at the right place. */
596 /* Write the left string, spaces to fill the gap, and the right string. */
597 put_barstr(&bar, line->p, render->leftsz);
598 put_barspc(&bar, render->width - render->leftwd - render->rightwd);
599 put_barstr(&bar, line->p + line->sz - render->rightsz, render->rightsz);
605 int ttyprogress_shownotice(struct ttyprogress_render *render,
606 const struct tty_attr *attr)
608 struct tty *tty = render->tty;
609 const struct ttyprogress_buffer *line = render->line;
611 /* If there's no terminal, then there's nothing to do. */
612 if (!tty->fpout) return (-1);
614 /* Set the attributes. */
615 tty_setattr(tty, attr);
617 /* Print the left and right strings. */
618 put_str(tty->fpout, line->p, render->leftsz);
619 put_spc(tty, render->width - render->leftwd - render->rightwd);
620 put_str(tty->fpout, line->p + line->sz - render->rightsz, render->rightsz);
626 /*----- That's all, folks -------------------------------------------------*/