Commit | Line | Data |
---|---|---|
293c860f MW |
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 | ||
dc53ebfa MW |
24 | #define _XOPEN_SOURCE |
25 | ||
293c860f MW |
26 | /*----- Header files ------------------------------------------------------*/ |
27 | ||
dc53ebfa MW |
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 | ||
293c860f MW |
47 | /*----- Progress state lifecycle ------------------------------------------*/ |
48 | ||
dc53ebfa | 49 | static FILE *dup_stream(int fd) |
293c860f MW |
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 | */ | |
dc53ebfa MW |
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 | ||
293c860f | 76 | /* Clear the progress state. */ |
dc53ebfa MW |
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 | ||
293c860f MW |
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 | */ | |
dc53ebfa MW |
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 | ||
293c860f MW |
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 | */ | |
dc53ebfa MW |
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 | ||
293c860f | 113 | /* Determine the terminal capabilities and size. */ |
dc53ebfa | 114 | #if defined(USE_TERMINFO) |
293c860f MW |
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 | */ | |
dc53ebfa | 119 | |
293c860f | 120 | /* Look up the terminal. */ |
dc53ebfa MW |
121 | if (setupterm(0, fileno(tty->fp), &err) != OK || err < 1) return (-1); |
122 | ||
293c860f | 123 | /* Basic cursor motion and erasure. */ |
dc53ebfa | 124 | tty->cap.cr = tigetstr("cr"); |
e7b7bd3b | 125 | tty->cap.nw = tigetstr("nel"); |
dc53ebfa MW |
126 | tty->cap.up = tigetstr("cuu1"); |
127 | tty->cap.ce = tigetstr("el"); | |
128 | tty->cap.cd = tigetstr("ed"); | |
129 | ||
130 | if (tigetnum("xmc") < 1) { | |
293c860f MW |
131 | /* No magic cookies, so check on the fancy highlighting. */ |
132 | ||
dc53ebfa MW |
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 | ||
293c860f | 142 | /* Find out whether erasure uses the current background colour. */ |
dc53ebfa MW |
143 | if (tigetflag("bce") > 0) tty->cap.f |= TCF_BCE; |
144 | ||
293c860f | 145 | /* Set the terminal size. */ |
dc53ebfa MW |
146 | SETDIM(defwd, "COLUMNS", tigetnum("co"), 80); |
147 | SETDIM(defht, "LINES", tigetnum("li"), 25); | |
148 | ||
149 | #elif defined(USE_TERMCAP) | |
293c860f MW |
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 | */ | |
dc53ebfa | 155 | |
293c860f | 156 | /* Look up the terminal. */ |
dc53ebfa MW |
157 | tty->termbuf = malloc(4096); if (!tty->termbuf) return (-1); |
158 | tty->capbuf = malloc(4096); if (!tty->capbuf) return (-1); | |
847b25bd MW |
159 | term = getenv("TERM"); if (!term) return (-1); |
160 | if (tgetent(tty->termbuf, term) < 1) return (-1); | |
dc53ebfa | 161 | capcur = tty->capbuf; |
293c860f MW |
162 | |
163 | /* Basic cursor motion and erasure. */ | |
dc53ebfa | 164 | tty->cap.cr = tgetstr("cr", &capcur); |
e7b7bd3b | 165 | tty->cap.nw = tgetstr("nw", &capcur); |
dc53ebfa MW |
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) { | |
293c860f MW |
171 | /* No magic cookies, so check on the fancy highlighting. */ |
172 | ||
dc53ebfa MW |
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 | ||
293c860f | 182 | /* Find out whether erasure uses the current background colour. */ |
dc53ebfa MW |
183 | if (tgetflag("ut") > 0) tty->cap.f |= TCF_BCE; |
184 | ||
293c860f | 185 | /* Set the pad character correctly. */ |
35951074 | 186 | t = tgetstr("pc", &capcur); tty->cap.pc = t ? *t : 0; |
dc53ebfa | 187 | |
293c860f | 188 | /* Set the terminal size. */ |
dc53ebfa MW |
189 | SETDIM(defwd, "COLUMNS", tgetnum("co"), 80); |
190 | SETDIM(defht, "LINES", tgetnum("li"), 25); | |
191 | ||
192 | #else | |
293c860f | 193 | /* Nothing to do. Take a wild guess at the terminal size. */ |
dc53ebfa MW |
194 | |
195 | SETDIM(defwd, "COLUMNS", -1, 80); | |
196 | SETDIM(defht, "LINES", -1, 25); | |
197 | ||
198 | #endif | |
199 | ||
200 | #undef SETDIM | |
201 | ||
293c860f MW |
202 | /* Fill in default motion. These are frequently omitted from capability |
203 | * strings. | |
204 | */ | |
b03fb933 | 205 | if (!tty->cap.cr) tty->cap.cr = "\r"; |
e7b7bd3b | 206 | if (!tty->cap.nw) tty->cap.nw = "\r\n"; |
293c860f MW |
207 | |
208 | /* If the terminal can't do the necessary motion and erasure then give up | |
209 | * on it. | |
210 | */ | |
b03fb933 | 211 | if (!tty->cap.up || !tty->cap.ce || !tty->cap.cd) |
dc53ebfa | 212 | { fclose(tty->fp); tty->fp = 0; return (-1); } |
293c860f MW |
213 | |
214 | /* If the terminal can't do all of the colour stuff we want, then clear | |
215 | * `op' as a hint. | |
216 | */ | |
dc53ebfa | 217 | if (!tty->cap.af || !tty->cap.ab || !tty->cap.op) tty->cap.op = 0; |
293c860f MW |
218 | |
219 | /* If the terminal can't return to normal, then clear bold and | |
220 | * reverse-video. | |
221 | */ | |
dc53ebfa | 222 | if (!tty->cap.me) tty->cap.mr = tty->cap.md = 0; |
293c860f MW |
223 | |
224 | /* Turn on full buffering. We take responsibility for forcing output at | |
225 | * the right times. | |
226 | */ | |
5d9660e0 | 227 | setvbuf(tty->fp, 0, _IOFBF, BUFSIZ); |
293c860f MW |
228 | |
229 | /* All done. */ | |
dc53ebfa MW |
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; } | |
b9c5f6a7 MW |
238 | free(tty->termbuf); tty->termbuf = 0; |
239 | free(tty->capbuf); tty->capbuf = 0; | |
dc53ebfa MW |
240 | } |
241 | ||
293c860f MW |
242 | /*----- Active item list maintenance --------------------------------------*/ |
243 | ||
44d94f48 MW |
244 | int progress_additem(struct progress_state *progress, |
245 | struct progress_item *item) | |
dc53ebfa | 246 | { |
44d94f48 MW |
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++; | |
dc53ebfa | 253 | |
dc53ebfa MW |
254 | return (0); |
255 | } | |
256 | ||
44d94f48 MW |
257 | int progress_removeitem(struct progress_state *progress, |
258 | struct progress_item *item) | |
dc53ebfa | 259 | { |
44d94f48 MW |
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; | |
dc53ebfa | 266 | |
dc53ebfa MW |
267 | return (0); |
268 | } | |
269 | ||
293c860f MW |
270 | /*----- Render state lifecycle --------------------------------------------*/ |
271 | ||
be805677 MW |
272 | static void setup_render_state(struct progress_state *progress, |
273 | struct progress_render_state *render) | |
dc53ebfa MW |
274 | { |
275 | const struct progress_ttyinfo *tty = &progress->tty; | |
276 | struct winsize wsz; | |
dc53ebfa | 277 | |
293c860f | 278 | /* Clear everything. */ |
dc53ebfa MW |
279 | render->tty = tty; |
280 | render->linebuf = 0; render->linesz = 0; | |
281 | render->tempbuf = 0; render->tempsz = 0; | |
282 | ||
283 | #ifdef USE_TERMCAP | |
293c860f MW |
284 | /* Save old `termcap' globals. We'll restore them in `free_render_state'. |
285 | */ | |
dc53ebfa MW |
286 | render->old_bc = BC; BC = 0; |
287 | render->old_up = UP; UP = 0; | |
35951074 | 288 | render->old_pc = PC; PC = tty->cap.pc; |
dc53ebfa MW |
289 | #endif |
290 | ||
293c860f MW |
291 | /* Determine the actual terminal size. Fall back on the default we |
292 | * established in `progress_init' if the kernel doesn't know. | |
293 | */ | |
dc53ebfa MW |
294 | if (!ioctl(fileno(tty->fp), TIOCGWINSZ, &wsz)) |
295 | { render->width = wsz.ws_col; render->height = wsz.ws_row; } | |
296 | else | |
be805677 | 297 | { render->width = tty->defwd; render->height = tty->defht; } |
dc53ebfa | 298 | |
293c860f MW |
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 | */ | |
dc53ebfa | 303 | if (render->width && !tty->cap.op && !tty->cap.mr) render->width--; |
dc53ebfa MW |
304 | } |
305 | ||
306 | static void free_render_state(struct progress_render_state *render) | |
307 | { | |
293c860f | 308 | /* Send accumulated output to the terminal. */ |
dc53ebfa | 309 | fflush(render->tty->fp); |
293c860f MW |
310 | |
311 | /* Free the buffers. */ | |
dc53ebfa MW |
312 | free(render->linebuf); render->linebuf = 0; render->linesz = 0; |
313 | free(render->tempbuf); render->tempbuf = 0; render->tempsz = 0; | |
293c860f | 314 | |
dc53ebfa | 315 | #ifdef USE_TERMCAP |
293c860f | 316 | /* Restore the `termcap' globals. */ |
dc53ebfa MW |
317 | UP = render->old_up; |
318 | BC = render->old_bc; | |
35951074 | 319 | PC = render->old_pc; |
dc53ebfa MW |
320 | #endif |
321 | } | |
322 | ||
293c860f MW |
323 | /*----- Measuring string widths -------------------------------------------*/ |
324 | ||
dc53ebfa MW |
325 | #define CONV_MORE ((size_t)-2) |
326 | #define CONV_BAD ((size_t)-1) | |
327 | ||
328 | struct measure { | |
293c860f MW |
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 */ | |
dc53ebfa MW |
332 | }; |
333 | ||
334 | static void init_measure(struct measure *m, const char *p, size_t sz) | |
293c860f | 335 | /* Set up M to measure the SZ-byte string P. */ |
dc53ebfa MW |
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) | |
293c860f MW |
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 | */ | |
dc53ebfa MW |
346 | { |
347 | wchar_t wch; | |
348 | unsigned chwd; | |
349 | size_t n; | |
350 | ||
293c860f MW |
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 | */ | |
dc53ebfa MW |
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 | ||
293c860f | 360 | /* Advance the state. */ |
dc53ebfa | 361 | m->i += n; m->wd += chwd; |
293c860f MW |
362 | |
363 | /* Report whether there's more to come. */ | |
dc53ebfa MW |
364 | return (m->i < m->sz); |
365 | } | |
366 | ||
367 | static unsigned string_width(const char *p, size_t sz) | |
293c860f | 368 | /* Return the width of the SZ-byte string P, in terminal columns. */ |
dc53ebfa MW |
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) | |
293c860f MW |
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 | */ | |
dc53ebfa MW |
383 | { |
384 | struct measure m; | |
12badb9a | 385 | size_t i; unsigned wd; |
dc53ebfa MW |
386 | int more; |
387 | ||
388 | init_measure(&m, p, sz); | |
293c860f MW |
389 | |
390 | /* Advance until we're past the bound. */ | |
dc53ebfa | 391 | for (;;) { |
12badb9a MW |
392 | if (!advance_measure(&m)) { *wd_out = m.wd; return (sz); } |
393 | if (m.wd >= maxwd) break; | |
394 | } | |
293c860f MW |
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 | */ | |
12badb9a MW |
400 | wd = m.wd; i = m.i; |
401 | for (;;) { | |
dc53ebfa | 402 | more = advance_measure(&m); |
12badb9a MW |
403 | if (m.wd > wd) break; |
404 | i = m.i; | |
405 | if (!more) break; | |
dc53ebfa | 406 | } |
293c860f MW |
407 | |
408 | /* All done. */ | |
12badb9a | 409 | *wd_out = wd; return (i); |
dc53ebfa MW |
410 | } |
411 | ||
293c860f MW |
412 | /*----- Output buffer handling --------------------------------------------*/ |
413 | ||
44d94f48 | 414 | static int grow_linebuf(struct progress_render_state *render, size_t want) |
293c860f MW |
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 | */ | |
44d94f48 MW |
420 | { |
421 | char *newbuf; size_t newsz; | |
422 | ||
293c860f | 423 | /* Return if there's already enough space. */ |
44d94f48 | 424 | if (want <= render->linesz) return (0); |
293c860f MW |
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 | */ | |
44d94f48 MW |
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; | |
293c860f MW |
437 | |
438 | /* Copy the left and right strings into the new buffer. */ | |
44d94f48 MW |
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); | |
293c860f MW |
445 | |
446 | /* Free the old buffer and remember the new one. */ | |
44d94f48 | 447 | free(render->linebuf); render->linebuf = newbuf; render->linesz = newsz; |
293c860f MW |
448 | |
449 | /* All done. */ | |
44d94f48 MW |
450 | return (0); |
451 | } | |
452 | ||
453 | static int grow_tempbuf(struct progress_render_state *render, size_t want) | |
293c860f MW |
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 | */ | |
44d94f48 MW |
458 | { |
459 | char *newbuf; size_t newsz; | |
460 | ||
293c860f | 461 | /* Return if there's already enough space. */ |
44d94f48 | 462 | if (want <= render->tempsz) return (0); |
293c860f MW |
463 | |
464 | /* Work out how much space to allocate. This is the same as `grow_linebuf' | |
465 | * above. | |
466 | */ | |
44d94f48 MW |
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; | |
293c860f MW |
472 | |
473 | /* Free the old buffer and keep the new one. */ | |
44d94f48 | 474 | free(render->tempbuf); render->tempbuf = newbuf; render->tempsz = newsz; |
293c860f MW |
475 | |
476 | /* All done. */ | |
44d94f48 MW |
477 | return (0); |
478 | } | |
479 | ||
dc53ebfa MW |
480 | enum { LEFT, RIGHT }; |
481 | static int putstr(struct progress_render_state *render, unsigned side, | |
482 | const char *p, size_t n) | |
293c860f MW |
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 | */ | |
dc53ebfa MW |
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) | |
293c860f MW |
510 | /* Format a `printf'-style string FMT with arguments AP, and add it |
511 | * to SIDE of RENDER's line buffer. | |
512 | */ | |
dc53ebfa MW |
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 | ||
293c860f MW |
558 | /*----- Terminal output ---------------------------------------------------*/ |
559 | ||
44d94f48 | 560 | #if defined(USE_TERMINFO) |
293c860f | 561 | |
44d94f48 MW |
562 | static const struct progress_ttyinfo *curtty = 0; |
563 | static int putty(int ch) { return (putc(ch, curtty->fp)); } | |
c45831b0 MW |
564 | void progress_put_sequence(const struct progress_ttyinfo *tty, |
565 | const char *p, unsigned nlines) | |
44d94f48 | 566 | { if (p) { curtty = tty; tputs(p, nlines, putty); } } |
c45831b0 MW |
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); } | |
293c860f | 571 | |
44d94f48 | 572 | #elif defined(USE_TERMCAP) |
293c860f | 573 | |
44d94f48 MW |
574 | static const struct progress_ttyinfo *curtty = 0; |
575 | static int putty(int ch) { return (putc(ch, curtty->fp)); } | |
c45831b0 MW |
576 | void progress_put_sequence(const struct progress_ttyinfo *tty, |
577 | const char *p, unsigned nlines) | |
44d94f48 | 578 | { if (p) { curtty = tty; tputs(p, nlines, putty); } } |
c45831b0 MW |
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); } | |
293c860f | 583 | |
44d94f48 | 584 | #else |
293c860f | 585 | |
c45831b0 MW |
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) | |
44d94f48 | 589 | { ; } |
c45831b0 | 590 | void progress_set_bgcolour(const struct progress_ttyinfo *tty, int colour) |
44d94f48 | 591 | { ; } |
293c860f | 592 | |
44d94f48 MW |
593 | #endif |
594 | ||
293c860f MW |
595 | /*----- Maintaining the progress display ----------------------------------*/ |
596 | ||
597 | #define CLRF_ALL 1u /* clear everything */ | |
0d5797b0 MW |
598 | static void clear_progress(struct progress_state *progress, |
599 | struct progress_render_state *render, unsigned f) | |
293c860f MW |
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 | */ | |
44d94f48 MW |
609 | { |
610 | const struct progress_ttyinfo *tty = &progress->tty; | |
611 | unsigned ndel, nleave; | |
612 | unsigned i; | |
613 | ||
c45831b0 | 614 | progress_put_sequence(tty, tty->cap.cr, 1); |
44d94f48 | 615 | if (progress->last_lines) { |
293c860f MW |
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 | */ | |
44d94f48 MW |
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 | } | |
293c860f MW |
628 | |
629 | /* Now actually do the clearing. Remember that the cursor is still on | |
630 | * the last line. | |
631 | */ | |
44d94f48 | 632 | if (!ndel) |
c45831b0 MW |
633 | for (i = 1; i < nleave; i++) |
634 | progress_put_sequence(tty, tty->cap.up, 1); | |
44d94f48 | 635 | else { |
c45831b0 MW |
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); | |
44d94f48 MW |
641 | } |
642 | } | |
293c860f MW |
643 | |
644 | /* Remember that we're now at the top of the display. */ | |
44d94f48 | 645 | progress->last_lines = 0; |
44d94f48 MW |
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); | |
be805677 | 653 | setup_render_state(progress, &render); |
44d94f48 MW |
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); | |
293c860f | 668 | |
be805677 | 669 | setup_render_state(progress, &render); |
44d94f48 MW |
670 | clear_progress(progress, &render, 0); |
671 | ||
672 | for (item = progress->items; item; item = item->next) { | |
c45831b0 | 673 | if (f&f_any) progress_put_sequence(tty, tty->cap.nw, 1); |
44d94f48 MW |
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 | } | |
c45831b0 | 679 | if (f&f_any) progress_put_sequence(tty, tty->cap.cr, 1); |
44d94f48 MW |
680 | free_render_state(&render); |
681 | return (0); | |
682 | } | |
683 | ||
293c860f MW |
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 | ||
dc53ebfa MW |
692 | enum { |
693 | LEFT_COLOUR, | |
694 | LEFT_MONO, | |
695 | LEFT_SIMPLE, | |
696 | RIGHT_ANY, | |
697 | STOP | |
698 | }; | |
699 | ||
700 | struct bar_state { | |
293c860f MW |
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 */ | |
dc53ebfa MW |
711 | }; |
712 | ||
713 | static void advance_bar_state(struct bar_state *bar) | |
293c860f MW |
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 | */ | |
dc53ebfa MW |
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 | ||
12badb9a | 723 | while (bar->nextpos <= here) { |
dc53ebfa | 724 | switch (bar->state) { |
c45831b0 MW |
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; | |
dc53ebfa MW |
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 | ||
293c860f | 734 | /* Little utilities to output a chunk of text, or some spaces. */ |
dc53ebfa MW |
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) | |
293c860f MW |
741 | /* Output the SZ-byte string P, driving the state machine BAR as we |
742 | * go. | |
743 | */ | |
dc53ebfa MW |
744 | { |
745 | unsigned wd; | |
746 | size_t n; | |
747 | ||
748 | for (;;) { | |
293c860f MW |
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 | ||
dc53ebfa MW |
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 | } | |
293c860f MW |
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 | */ | |
dc53ebfa MW |
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) | |
293c860f | 767 | /* Output N spaces, driving the state machine BAR as we go. */ |
dc53ebfa MW |
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 | ||
293c860f | 786 | /* If there's no terminal, then there's nothing to do. */ |
dc53ebfa MW |
787 | if (!tty->fp) return (-1); |
788 | ||
293c860f | 789 | /* Set up the render state, with a transition where the bar should end. */ |
dc53ebfa MW |
790 | bar.render = render; bar.pos = 0; bar.nextpos = frac*render->width + 0.5; |
791 | ||
293c860f | 792 | /* Set the initial state for the render. */ |
dc53ebfa | 793 | if (tty->cap.op) { |
293c860f MW |
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 | */ | |
c45831b0 MW |
800 | progress_set_fgcolour(tty, 0); bar.state = LEFT_COLOUR; |
801 | if (bar.nextpos) progress_set_bgcolour(tty, 2); | |
dc53ebfa MW |
802 | else advance_bar_state(&bar); |
803 | } else if (tty->cap.mr) { | |
293c860f MW |
804 | /* We have reverse-video. Write the progress bar in reverse and the rest |
805 | * in normal. | |
806 | */ | |
807 | ||
dc53ebfa | 808 | if (bar.nextpos) |
c45831b0 | 809 | { bar.state = LEFT_MONO; progress_put_sequence(tty, tty->cap.mr, 1); } |
dc53ebfa MW |
810 | else |
811 | { bar.state = RIGHT; bar.nextpos = render->width; } | |
812 | } else | |
293c860f | 813 | /* Nothing fancy. We'll write `|' at the right place. */ |
dc53ebfa MW |
814 | bar.state = LEFT_SIMPLE; |
815 | ||
293c860f | 816 | /* Write the left string, spaces to fill the gap, and the right string. */ |
dc53ebfa MW |
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 | ||
293c860f | 823 | /* Final output: turn off fancy highlighting, and colours. */ |
c45831b0 MW |
824 | progress_put_sequence(tty, tty->cap.me, 1); |
825 | progress_put_sequence(tty, tty->cap.op, 1); | |
dc53ebfa | 826 | |
293c860f | 827 | /* All done. */ |
dc53ebfa MW |
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 | ||
293c860f | 835 | /* If there's no terminal, then there's nothing to do. */ |
dc53ebfa MW |
836 | if (!tty->fp) return (-1); |
837 | ||
293c860f MW |
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 | ||
c45831b0 | 846 | progress_put_sequence(tty, tty->cap.mr, 1); |
293c860f MW |
847 | } |
848 | ||
849 | /* Set boldface. (If we have it.) */ | |
9c98b3ee | 850 | progress_put_sequence(tty, tty->cap.md, 1); |
dc53ebfa | 851 | |
293c860f MW |
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 | */ | |
dc53ebfa MW |
856 | put_str(tty->fp, render->linebuf, render->leftsz); |
857 | if (!render->rightsz && (tty->cap.f&TCF_BCE) && tty->cap.ce) | |
c45831b0 | 858 | progress_put_sequence(tty, tty->cap.ce, 1); |
dc53ebfa MW |
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 | ||
293c860f | 866 | /* Put things back to normal. */ |
c45831b0 MW |
867 | progress_put_sequence(tty, tty->cap.me, 1); |
868 | progress_put_sequence(tty, tty->cap.op, 1); | |
dc53ebfa | 869 | |
293c860f | 870 | /* All done. */ |
dc53ebfa MW |
871 | return (0); |
872 | } | |
293c860f MW |
873 | |
874 | /*----- That's all, folks -------------------------------------------------*/ |