1 /*- 2 * SPDX-License-Identifier: BSD-2-Clause 3 * 4 * Copyright (c) 2021-2023 Alfonso Sabato Siciliano 5 * 6 * Redistribution and use in source and binary forms, with or without 7 * modification, are permitted provided that the following conditions 8 * are met: 9 * 1. Redistributions of source code must retain the above copyright 10 * notice, this list of conditions and the following disclaimer. 11 * 2. Redistributions in binary form must reproduce the above copyright 12 * notice, this list of conditions and the following disclaimer in the 13 * documentation and/or other materials provided with the distribution. 14 * 15 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 16 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 18 * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 19 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 21 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 22 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 23 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 24 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 25 * SUCH DAMAGE. 26 */ 27 28 #include <curses.h> 29 #include <stdlib.h> 30 #include <string.h> 31 #include <time.h> 32 #include <unistd.h> 33 34 #include "bsddialog.h" 35 #include "bsddialog_progressview.h" 36 #include "bsddialog_theme.h" 37 #include "lib_util.h" 38 39 #define BARPADDING 2 /* widget border | BARPADDING | box bar */ 40 #define BOXBORDERS 2 41 #define MIN_WBAR 15 42 #define MIN_WBOX (BARPADDING + BOXBORDERS + MIN_WBAR + BARPADDING) 43 #define MIN_WMGBAR 18 44 #define MIN_WMGBOX (BARPADDING + BOXBORDERS + MIN_WMGBAR + BARPADDING) 45 #define HBOX 3 46 #define WBOX(d) ((d)->w - BORDERS - BARPADDING - BARPADDING) 47 #define WBAR(d) (WBOX(d) - BOXBORDERS) 48 49 bool bsddialog_interruptprogview; 50 bool bsddialog_abortprogview; 51 long long int bsddialog_total_progview; 52 53 static const char states[12][14] = { 54 " Succeeded ", /* -1 */ 55 " Failed ", /* -2 */ 56 " Passed ", /* -3 */ 57 " Completed ", /* -4 */ 58 " Checked ", /* -5 */ 59 " Done ", /* -6 */ 60 " Skipped ", /* -7 */ 61 " In Progress ", /* -8 */ 62 "(blank) ", /* -9 */ 63 " N/A ", /* -10 */ 64 " Pending ", /* -11 */ 65 " UNKNOWN ", /* < -11, no API */ 66 }; 67 68 struct bar { 69 bool toupdate; 70 WINDOW *win; 71 int y; /* bar y in win */ 72 int x; /* bar x in win */ 73 int w; /* width in win */ 74 int perc; /* barlen = (w * perc) / 100 */ 75 const char* fmt; /* format for label */ 76 int label; /* rangebox and pause perc!=label */ 77 }; 78 79 static void draw_bar(struct bar *b) 80 { 81 int barlen, xlabel; 82 chtype ch; 83 char label[128]; 84 85 barlen = b->perc > 0 ? (b->perc * b->w) / 100 : 0; 86 87 ch = ' ' | t.bar.f_color; 88 mvwhline(b->win, b->y, b->x, ch, barlen); 89 ch = ' ' | t.bar.color; 90 mvwhline(b->win, b->y, b->x + barlen, ch, b->w - barlen); 91 92 sprintf(label, b->fmt, b->label); 93 xlabel = b->x + b->w/2 - (int)strlen(label)/2; /* 1-byte-char string */ 94 wattron(b->win, t.bar.color); /* x+barlen < xlabel */ 95 mvwaddstr(b->win, b->y, xlabel, label); 96 wattroff(b->win, t.bar.color); 97 wattron(b->win, t.bar.f_color); /* x+barlen >= xlabel */ 98 mvwaddnstr(b->win, b->y, xlabel, label, MAX((b->x+barlen) - xlabel, 0)); 99 wattroff(b->win, t.bar.f_color); 100 101 if (b->toupdate) 102 wnoutrefresh(b->win); 103 b->toupdate = false; 104 } 105 106 static void update_barbox(struct dialog *d, struct bar *b, bool buttons) 107 { 108 int y; 109 110 y = d->y + d->h - BORDER - HBOX; 111 if (buttons) 112 y -= HBUTTONS; 113 update_box(d->conf, b->win, y, d->x + BORDER + BARPADDING, HBOX, 114 WBOX(d), RAISED); 115 } 116 117 int 118 bsddialog_gauge(struct bsddialog_conf *conf, const char *text, int rows, 119 int cols, unsigned int perc, int fd, const char *sep, const char *end) 120 { 121 bool mainloop; 122 int fd2; 123 FILE *input; 124 char inputbuf[2048], ntext[2048], *pntext; 125 struct bar b; 126 struct dialog d; 127 128 if (prepare_dialog(conf, text, rows, cols, &d) != 0) 129 return (BSDDIALOG_ERROR); 130 if ((b.win = newwin(1, 1, 1, 1)) == NULL) 131 RETURN_ERROR("Cannot build WINDOW bar"); 132 b.y = b.x = 1; 133 b.fmt = "%3d%%"; 134 135 input = NULL; 136 if (fd >= 0) { 137 CHECK_PTR(sep); 138 CHECK_PTR(end); 139 140 fd2 = dup(fd); 141 if ((input = fdopen(fd2, "r")) == NULL) 142 RETURN_FMTERROR("Cannot build FILE* from fd %d", fd); 143 } 144 145 perc = MIN(perc, 100); 146 mainloop = true; 147 while (mainloop) { 148 if (d.built) { 149 hide_dialog(&d); 150 refresh(); /* Important for decreasing screen */ 151 } 152 if (dialog_size_position(&d, HBOX, MIN_WBOX, NULL) != 0) 153 return (BSDDIALOG_ERROR); 154 if (draw_dialog(&d)) 155 return (BSDDIALOG_ERROR); 156 if (d.built) 157 refresh(); /* fix grey lines expanding screen */ 158 TEXTPAD(&d, HBOX); 159 update_barbox(&d, &b, false); 160 b.w = WBAR(&d); 161 b.perc = b.label = perc; 162 b.toupdate = true; 163 draw_bar(&b); 164 doupdate(); 165 if (input == NULL) /* that is fd < 0 */ 166 break; 167 168 while (true) { 169 fscanf(input, "%s", inputbuf); 170 if (strcmp(inputbuf, end) == 0) { 171 mainloop = false; 172 break; 173 } 174 if (strcmp(inputbuf, sep) == 0) 175 break; 176 } 177 if (mainloop == false) 178 break; 179 fscanf(input, "%d", &perc); 180 perc = MIN(perc, 100); 181 pntext = &ntext[0]; 182 ntext[0] = '\0'; 183 while (true) { 184 fscanf(input, "%s", inputbuf); 185 if (strcmp(inputbuf, end) == 0) { 186 mainloop = false; 187 break; 188 } 189 if (strcmp(inputbuf, sep) == 0) 190 break; 191 strcpy(pntext, inputbuf); 192 pntext += strlen(inputbuf); /* end string, no strlen */ 193 pntext[0] = ' '; 194 pntext++; 195 } 196 pntext[0] = '\0'; 197 d.text = ntext; 198 } 199 200 if (input != NULL) 201 fclose(input); 202 delwin(b.win); 203 end_dialog(&d); 204 205 return (BSDDIALOG_OK); 206 } 207 208 /* Mixedgauge */ 209 static int 210 do_mixedgauge(struct bsddialog_conf *conf, const char *text, int rows, int cols, 211 unsigned int mainperc, unsigned int nminibars, const char **minilabels, 212 int *minipercs, bool color) 213 { 214 int i, miniperc, max_minibarlen; 215 int ystext, htext; 216 int minicolor, red, green; 217 struct bar b; 218 struct dialog d; 219 220 CHECK_ARRAY(nminibars, minilabels); 221 CHECK_ARRAY(nminibars, minipercs); 222 223 red = bsddialog_color(BSDDIALOG_WHITE,BSDDIALOG_RED, BSDDIALOG_BOLD); 224 green = bsddialog_color(BSDDIALOG_WHITE,BSDDIALOG_GREEN,BSDDIALOG_BOLD); 225 226 max_minibarlen = 0; 227 for (i = 0; i < (int)nminibars; i++) 228 max_minibarlen = MAX(max_minibarlen, 229 (int)strcols(CHECK_STR(minilabels[i]))); 230 max_minibarlen += 3 + 16; /* seps + [...] */ 231 max_minibarlen = MAX(max_minibarlen, MIN_WMGBOX); /* mainbar */ 232 233 if (prepare_dialog(conf, text, rows, cols, &d) != 0) 234 return (BSDDIALOG_ERROR); 235 if (dialog_size_position(&d, nminibars + HBOX, max_minibarlen, 236 &htext) != 0) 237 return (BSDDIALOG_ERROR); 238 if (draw_dialog(&d) != 0) 239 return (BSDDIALOG_ERROR); 240 241 /* mini bars */ 242 b.win = d.widget; 243 b.x = 1 + d.w - 2 - 15; 244 b.w = 13; 245 b.fmt = "%3d%%"; 246 b.toupdate = false; 247 for (i = 0; i < (int)nminibars; i++) { 248 miniperc = minipercs[i]; 249 /* label */ 250 if (color && miniperc >= 0) 251 wattron(d.widget, A_BOLD); 252 mvwaddstr(d.widget, i+1, 2, CHECK_STR(minilabels[i])); 253 if (color && miniperc >= 0) 254 wattroff(d.widget, A_BOLD); 255 /* perc */ 256 if (miniperc == BSDDIALOG_MG_BLANK) 257 continue; 258 mvwaddstr(d.widget, i+1, d.w-2-15, "[ ]"); 259 if (miniperc >= 0) { 260 b.y = i + 1; 261 b.perc = b.label = MIN(miniperc, 100); 262 draw_bar(&b); 263 } else { /* miniperc < 0 */ 264 if (miniperc < BSDDIALOG_MG_PENDING) 265 miniperc = -12; /* UNKNOWN */ 266 minicolor = t.dialog.color; 267 if (color && miniperc == BSDDIALOG_MG_FAILED) 268 minicolor = red; 269 else if (color && miniperc == BSDDIALOG_MG_DONE) 270 minicolor = green; 271 wattron(d.widget, minicolor); 272 miniperc = abs(miniperc + 1); 273 mvwaddstr(d.widget, i+1, 1+d.w-2-15, states[miniperc]); 274 wattroff(d.widget, minicolor); 275 } 276 } 277 wnoutrefresh(d.widget); 278 279 /* text */ 280 ystext = MAX(d.h - BORDERS - htext - HBOX, (int)nminibars); 281 rtextpad(&d, 0, 0, ystext, HBOX); 282 283 /* main bar */ 284 if ((b.win = newwin(1, 1, 1, 1)) == NULL) 285 RETURN_ERROR("Cannot build WINDOW bar"); 286 update_barbox(&d, &b, false); 287 wattron(b.win, t.bar.color); 288 mvwaddstr(b.win, 0, 2, "Overall Progress"); 289 wattroff(b.win, t.bar.color); 290 291 b.y = b.x = 1; 292 b.w = WBAR(&d); 293 b.fmt = "%3d%%"; 294 b.perc = b.label = MIN(mainperc, 100); 295 b.toupdate = true; 296 draw_bar(&b); 297 298 doupdate(); 299 /* getch(); to test with "alternate mode" */ 300 301 delwin(b.win); 302 end_dialog(&d); 303 304 return (BSDDIALOG_OK); 305 } 306 307 int 308 bsddialog_mixedgauge(struct bsddialog_conf *conf, const char *text, int rows, 309 int cols, unsigned int mainperc, unsigned int nminibars, 310 const char **minilabels, int *minipercs) 311 { 312 int retval; 313 314 retval = do_mixedgauge(conf, text, rows, cols, mainperc, nminibars, 315 minilabels, minipercs, false); 316 317 return (retval); 318 } 319 320 int 321 bsddialog_progressview (struct bsddialog_conf *conf, const char *text, int rows, 322 int cols, struct bsddialog_progviewconf *pvconf, unsigned int nminibar, 323 struct bsddialog_fileminibar *minibar) 324 { 325 bool update; 326 int perc, retval, *minipercs; 327 unsigned int i, mainperc, totaltodo; 328 float readforsec; 329 const char **minilabels; 330 time_t tstart, told, tnew, refresh; 331 332 if ((minilabels = calloc(nminibar, sizeof(char*))) == NULL) 333 RETURN_ERROR("Cannot allocate memory for minilabels"); 334 if ((minipercs = calloc(nminibar, sizeof(int))) == NULL) 335 RETURN_ERROR("Cannot allocate memory for minipercs"); 336 337 totaltodo = 0; 338 for (i = 0; i < nminibar; i++) { 339 totaltodo += minibar[i].size; 340 minilabels[i] = minibar[i].label; 341 minipercs[i] = minibar[i].status; 342 } 343 344 refresh = pvconf->refresh == 0 ? 0 : pvconf->refresh - 1; 345 retval = BSDDIALOG_OK; 346 i = 0; 347 update = true; 348 time(&told); 349 tstart = told; 350 while (!(bsddialog_interruptprogview || bsddialog_abortprogview)) { 351 if (bsddialog_total_progview == 0 || totaltodo == 0) 352 mainperc = 0; 353 else 354 mainperc = (bsddialog_total_progview * 100) / totaltodo; 355 356 time(&tnew); 357 if (update || tnew > told + refresh) { 358 retval = do_mixedgauge(conf, text, rows, cols, mainperc, 359 nminibar, minilabels, minipercs, true); 360 if (retval == BSDDIALOG_ERROR) 361 return (BSDDIALOG_ERROR); 362 363 move(SCREENLINES - 1, 2); 364 clrtoeol(); 365 readforsec = ((tnew - tstart) == 0) ? 0 : 366 bsddialog_total_progview / (float)(tnew - tstart); 367 printw(pvconf->fmtbottomstr, bsddialog_total_progview, 368 readforsec); 369 refresh(); 370 371 time(&told); 372 update = false; 373 } 374 375 if (i >= nminibar) 376 break; 377 if (minibar[i].status == BSDDIALOG_MG_FAILED) 378 break; 379 380 perc = pvconf->callback(&minibar[i]); 381 382 if (minibar[i].status == BSDDIALOG_MG_DONE) { /*||perc >= 100)*/ 383 minipercs[i] = BSDDIALOG_MG_DONE; 384 update = true; 385 i++; 386 } else if (minibar[i].status == BSDDIALOG_MG_FAILED || 387 perc < 0) { 388 minipercs[i] = BSDDIALOG_MG_FAILED; 389 update = true; 390 } else /* perc >= 0 */ 391 minipercs[i] = perc; 392 } 393 394 free(minilabels); 395 free(minipercs); 396 return (retval); 397 } 398 399 static int rangebox_redraw(struct dialog *d, struct bar *b, int *bigchange) 400 { 401 if (d->built) { 402 hide_dialog(d); 403 refresh(); /* Important for decreasing screen */ 404 } 405 if (dialog_size_position(d, HBOX, MIN_WBOX, NULL) != 0) 406 return (BSDDIALOG_ERROR); 407 if (draw_dialog(d) != 0) 408 return (BSDDIALOG_ERROR); 409 if (d->built) 410 refresh(); /* Important to fix grey lines expanding screen */ 411 TEXTPAD(d, HBOX + HBUTTONS); 412 413 b->w = WBAR(d); 414 *bigchange = MAX(1, b->w / 10); 415 update_barbox(d, b, true); 416 b->toupdate = true; 417 418 return (0); 419 } 420 421 int 422 bsddialog_rangebox(struct bsddialog_conf *conf, const char *text, int rows, 423 int cols, int min, int max, int *value) 424 { 425 bool loop; 426 int currvalue, retval, bigchange, positions; 427 wint_t input; 428 struct bar b; 429 struct dialog d; 430 431 CHECK_PTR(value); 432 if (min >= max) 433 RETURN_FMTERROR("min (%d) >= max (%d)", min, max); 434 if (*value < min) 435 RETURN_FMTERROR("value (%d) < min (%d)", *value, min); 436 if (*value > max) 437 RETURN_FMTERROR("value (%d) > max (%d)", *value, max); 438 439 currvalue = *value; 440 positions = max - min + 1; 441 442 if (prepare_dialog(conf, text, rows, cols, &d) != 0) 443 return (BSDDIALOG_ERROR); 444 set_buttons(&d, true, OK_LABEL, CANCEL_LABEL); 445 if ((b.win = newwin(1, 1, 1, 1)) == NULL) 446 RETURN_ERROR("Cannot build WINDOW bar"); 447 b.y = b.x = 1; 448 b.fmt = "%d"; 449 if (rangebox_redraw(&d, &b, &bigchange) != 0) 450 return (BSDDIALOG_ERROR); 451 452 loop = true; 453 while (loop) { 454 if (b.toupdate) { 455 b.perc = ((float)(currvalue - min)*100) / (positions-1); 456 b.label = currvalue; 457 draw_bar(&b); 458 } 459 doupdate(); 460 if (get_wch(&input) == ERR) 461 continue; 462 switch(input) { 463 case KEY_ENTER: 464 case 10: /* Enter */ 465 retval = BUTTONVALUE(d.bs); 466 loop = false; 467 break; 468 case 27: /* Esc */ 469 if (conf->key.enable_esc) { 470 retval = BSDDIALOG_ESC; 471 loop = false; 472 } 473 break; 474 case '\t': /* TAB */ 475 case KEY_RIGHT: 476 d.bs.curr = (d.bs.curr + 1) % d.bs.nbuttons; 477 DRAW_BUTTONS(d); 478 break; 479 case KEY_LEFT: 480 d.bs.curr--; 481 if (d.bs.curr < 0) 482 d.bs.curr = d.bs.nbuttons - 1; 483 DRAW_BUTTONS(d); 484 break; 485 case KEY_HOME: 486 currvalue = max; 487 b.toupdate = true; 488 break; 489 case KEY_END: 490 currvalue = min; 491 b.toupdate = true; 492 break; 493 case KEY_NPAGE: 494 currvalue -= bigchange; 495 if (currvalue < min) 496 currvalue = min; 497 b.toupdate = true; 498 break; 499 case KEY_PPAGE: 500 currvalue += bigchange; 501 if (currvalue > max) 502 currvalue = max; 503 b.toupdate = true; 504 break; 505 case KEY_UP: 506 if (currvalue < max) { 507 currvalue++; 508 b.toupdate = true; 509 } 510 break; 511 case KEY_DOWN: 512 if (currvalue > min) { 513 currvalue--; 514 b.toupdate = true; 515 } 516 break; 517 case KEY_F(1): 518 if (conf->key.f1_file == NULL && 519 conf->key.f1_message == NULL) 520 break; 521 if (f1help_dialog(conf) != 0) 522 return (BSDDIALOG_ERROR); 523 if (rangebox_redraw(&d, &b, &bigchange) != 0) 524 return (BSDDIALOG_ERROR); 525 break; 526 case KEY_RESIZE: 527 if (rangebox_redraw(&d, &b, &bigchange) != 0) 528 return (BSDDIALOG_ERROR); 529 break; 530 default: 531 if (shortcut_buttons(input, &d.bs)) { 532 DRAW_BUTTONS(d); 533 doupdate(); 534 retval = BUTTONVALUE(d.bs); 535 loop = false; 536 } 537 } 538 } 539 540 *value = currvalue; 541 542 delwin(b.win); 543 end_dialog(&d); 544 545 return (retval); 546 } 547 548 static int pause_redraw(struct dialog *d, struct bar *b) 549 { 550 if (d->built) { 551 hide_dialog(d); 552 refresh(); /* Important for decreasing screen */ 553 } 554 if (dialog_size_position(d, HBOX, MIN_WBOX, NULL) != 0) 555 return (BSDDIALOG_ERROR); 556 if (draw_dialog(d) != 0) 557 return (BSDDIALOG_ERROR); 558 if (d->built) 559 refresh(); /* Important to fix grey lines expanding screen */ 560 TEXTPAD(d, HBOX + HBUTTONS); 561 562 b->w = WBAR(d); 563 update_barbox(d, b, true); 564 b->toupdate = true; 565 566 return (0); 567 } 568 569 int 570 bsddialog_pause(struct bsddialog_conf *conf, const char *text, int rows, 571 int cols, unsigned int *seconds) 572 { 573 bool loop; 574 int retval, tout; 575 wint_t input; 576 struct bar b; 577 struct dialog d; 578 579 CHECK_PTR(seconds); 580 if (prepare_dialog(conf, text, rows, cols, &d) != 0) 581 return (BSDDIALOG_ERROR); 582 set_buttons(&d, true, OK_LABEL, CANCEL_LABEL); 583 if ((b.win = newwin(1, 1, 1, 1)) == NULL) 584 RETURN_ERROR("Cannot build WINDOW bar"); 585 b.y = b.x = 1; 586 b.fmt = "%d"; 587 if (pause_redraw(&d, &b) != 0) 588 return (BSDDIALOG_ERROR); 589 590 tout = *seconds; 591 nodelay(stdscr, TRUE); 592 timeout(1000); 593 loop = true; 594 while (loop) { 595 if (b.toupdate) { 596 b.perc = (float)tout * 100 / *seconds; 597 b.label = tout; 598 draw_bar(&b); 599 } 600 doupdate(); 601 if (get_wch(&input) == ERR) { /* timeout */ 602 tout--; 603 if (tout < 0) { 604 retval = BSDDIALOG_TIMEOUT; 605 break; 606 } 607 else { 608 b.toupdate = true; 609 continue; 610 } 611 } 612 switch(input) { 613 case KEY_ENTER: 614 case 10: /* Enter */ 615 retval = BUTTONVALUE(d.bs); 616 loop = false; 617 break; 618 case 27: /* Esc */ 619 if (conf->key.enable_esc) { 620 retval = BSDDIALOG_ESC; 621 loop = false; 622 } 623 break; 624 case '\t': /* TAB */ 625 case KEY_RIGHT: 626 d.bs.curr = (d.bs.curr + 1) % d.bs.nbuttons; 627 DRAW_BUTTONS(d); 628 break; 629 case KEY_LEFT: 630 d.bs.curr--; 631 if (d.bs.curr < 0) 632 d.bs.curr = d.bs.nbuttons - 1; 633 DRAW_BUTTONS(d); 634 break; 635 case KEY_F(1): 636 if (conf->key.f1_file == NULL && 637 conf->key.f1_message == NULL) 638 break; 639 if (f1help_dialog(conf) != 0) 640 return (BSDDIALOG_ERROR); 641 if (pause_redraw(&d, &b) != 0) 642 return (BSDDIALOG_ERROR); 643 break; 644 case KEY_RESIZE: 645 if (pause_redraw(&d, &b) != 0) 646 return (BSDDIALOG_ERROR); 647 break; 648 default: 649 if (shortcut_buttons(input, &d.bs)) { 650 DRAW_BUTTONS(d); 651 doupdate(); 652 retval = BUTTONVALUE(d.bs); 653 loop = false; 654 } 655 } 656 } 657 nodelay(stdscr, FALSE); 658 659 *seconds = MAX(tout, 0); 660 661 delwin(b.win); 662 end_dialog(&d); 663 664 return (retval); 665 } 666