diff --git a/contrib/bsddialog/CHANGELOG b/contrib/bsddialog/CHANGELOG --- a/contrib/bsddialog/CHANGELOG +++ b/contrib/bsddialog/CHANGELOG @@ -1,3 +1,57 @@ +2026-07-08 Version 1.2 + + Library: + * fix: exit with ESC when stdin is hung up or at EOF (e.g. hosting + xterm closed) instead of spinning at 100% CPU on get_wch() == ERR; + add dialog_stdin_hungup() and DIALOG_INPUT_HUNGUP() + * add: alternate-screen drawing in bsddialog_init(); + bsddialog_keep_tite() for callers (e.g. bsdpv) that manage the TITE + buffer themselves + * add: backdrop and flush helpers: bsddialog_fill_backdrop(), + bsddialog_paint_screen(), dialog_backdrop_sync(), dialog_flush(), + dialog_flush_overlay(), dialog_flush_widget(), dialog_flush_stdscr(), + dialog_flush_reset(), paint_screen(), paint_screen_backdrop() + * add: persistent gauge API: bsddialog_gauge_open(), + bsddialog_gauge_update(), bsddialog_gauge_close(), + bsddialog_gauge_flush() + * add: dialog_repaint_frame(), dialog_repaint_text(), dialog_place(), + dialog_paint_frame(), textpad_to_widget(), and + bsddialog_set_default_theme_noflush() + * add: KEY_BTAB (Shift+Tab) backward focus navigation in all dialogs + with buttons or multi-pane focus (form, menu, calendar, slider, + timebox, datebox, msgbox, textbox, rangebox, pause, ...) + * add: BUTTONS_TAB_FORWARD() and BUTTONS_TAB_BACKWARD() helpers + * fix: scrollback pollution: prefer werase(3) over wclear(3); paint the + shadow as an L-shape instead of filling the whole shadow window; + untouchwin(3) stdscr(3) before doupdate(3) so bkgd(3) does not emit + clear_screen (CSI-H CSI-2J) into scrollback + * fix: textpad copywin(3) off-by-one (stray reverse-video cell after a + full-width line); rtextpad(3) width now matches the textpad; erase the + widget text band before copywin(3); print_textpad(3) newline/space + handling at pad width + * fix: calendar opens with the day grid focused instead of the buttons + * fix: correct noeol files bsddialog_progressview.h bsddialog_theme.h + datebox.c lib_util.c libbsddialog.c + * change: bsddialog_backtitle_rf() uses dialog_flush_stdscr() + * change: theme install no longer wnoutrefresh(3) stdscr(3) immediately; + bsddialog_get_theme() copies colours without applying bkgd(3) + * change: replace refresh()/doupdate() call sites with the new flush + helpers throughout the library + + Examples (library): + * add: clean + * fix: compile + - In FreeBSD tree OBJDIR precludes use of libdir=../lib + - Remap libdir to system library path when appropriate + - -I$libdir is confusing; use -I$incdir instead + - Remove unnecessary brackets in ${e}.c + + Examples (utility): + * change: gauge.sh use awk(1) NF built-in instead of split() return + * change: gauge.sh and mixedgauge.sh use $(( ... )) instead of expr(1) + * change: pause.sh and rangebox.sh use one extra row (7 → 8); show text + * change: mark calendar.sh, rangebox.sh, and slider.sh executable + 2025-08-15 Version 1.1 Library: diff --git a/contrib/bsddialog/examples_library/clean b/contrib/bsddialog/examples_library/clean new file mode 100755 --- /dev/null +++ b/contrib/bsddialog/examples_library/clean @@ -0,0 +1,9 @@ +#!/bin/sh +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2026 Devin Teske +# +set -x +rm -f menu checklist radiolist mixedlist theme infobox yesno msgbox datebox \ + form timebox rangebox pause calendar gauge mixedgauge textbox slider diff --git a/contrib/bsddialog/examples_library/compile b/contrib/bsddialog/examples_library/compile --- a/contrib/bsddialog/examples_library/compile +++ b/contrib/bsddialog/examples_library/compile @@ -10,14 +10,23 @@ set -x +library=bsddialog libpath=../lib +incpath=$libpath +if [ ! -e "$libpath/lib$library.so" ]; then + # In FreeBSD tree, library is in architecture dependent OBJDIR + # Compile against system-installed copy when operating in-tree + library=privatebsddialog + libpath=/usr/lib + incpath=/usr/include/private/bsddialog +fi examples="menu checklist radiolist mixedlist theme infobox yesno msgbox \ datebox form timebox rangebox pause calendar gauge mixedgauge textbox slider" -rm -f $examples +./clean for e in $examples do - cc -g -Wall -Wextra -I$libpath ${e}.c -o $e \ - -Wl,-rpath=$libpath -L$libpath -lbsddialog + cc -g -Wall -Wextra -I$incpath $e.c -o $e \ + -Wl,-rpath=$libpath -L$libpath -l$library done diff --git a/contrib/bsddialog/examples_utility/gauge.sh b/contrib/bsddialog/examples_utility/gauge.sh --- a/contrib/bsddialog/examples_utility/gauge.sh +++ b/contrib/bsddialog/examples_utility/gauge.sh @@ -10,13 +10,13 @@ # . characters="A B C D E F G" -total=`echo $characters | awk '{print split($0, a)}'` +total=$( echo $characters | awk '{print NF}' ) i=1 for c in $characters do sleep 1 echo XXX - echo "$(expr $(expr $i "*" 100) "/" $total)" + echo "$(( ($i * 100) / $total ))" echo "[$i/$total] Char: $c" echo XXX if [ $i -eq $total ] @@ -24,5 +24,5 @@ sleep 1 echo EOF fi - i=`expr $i + 1` + i=$(( $i + 1 )) done | ./bsddialog --title " gauge " --gauge "[0/$total] Starting..." 10 70 diff --git a/contrib/bsddialog/examples_utility/mixedgauge.sh b/contrib/bsddialog/examples_utility/mixedgauge.sh --- a/contrib/bsddialog/examples_utility/mixedgauge.sh +++ b/contrib/bsddialog/examples_utility/mixedgauge.sh @@ -28,6 +28,6 @@ "Label 11" " -11" \ "Label X" $perc - perc=`expr $perc + 20` - mainperc=`expr $mainperc + 10` + perc=$(( $perc + 20 )) + mainperc=$(( $mainperc + 10 )) done diff --git a/contrib/bsddialog/examples_utility/pause.sh b/contrib/bsddialog/examples_utility/pause.sh --- a/contrib/bsddialog/examples_utility/pause.sh +++ b/contrib/bsddialog/examples_utility/pause.sh @@ -15,7 +15,7 @@ : ${BSDDIALOG_TIMEOUT=4} : ${BSDDIALOG_ESC=5} -./bsddialog --title " pause " --pause "Hello World!" 7 35 10 +./bsddialog --title " pause " --pause "Hello World!" 8 35 10 case $? in $BSDDIALOG_ERROR ) diff --git a/contrib/bsddialog/examples_utility/rangebox.sh b/contrib/bsddialog/examples_utility/rangebox.sh --- a/contrib/bsddialog/examples_utility/rangebox.sh +++ b/contrib/bsddialog/examples_utility/rangebox.sh @@ -14,7 +14,7 @@ : ${BSDDIALOG_CANCEL=1} : ${BSDDIALOG_ESC=5} -VALUE=$(./bsddialog --title " rangebox " --rangebox "Hello World!" 7 35 0 10 5 \ +VALUE=$(./bsddialog --title " rangebox " --rangebox "Hello World!" 8 35 0 10 5 \ 3>&1 1>&2 2>&3 3>&-) case $? in diff --git a/contrib/bsddialog/lib/barbox.c b/contrib/bsddialog/lib/barbox.c --- a/contrib/bsddialog/lib/barbox.c +++ b/contrib/bsddialog/lib/barbox.c @@ -2,6 +2,7 @@ * SPDX-License-Identifier: BSD-2-Clause * * Copyright (c) 2021-2025 Alfonso Sabato Siciliano + * Copyright (c) 2026 Devin Teske * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions @@ -76,6 +77,72 @@ int label; /* rangebox and pause perc!=label */ }; +static struct { + bool active; + bool sized; + struct dialog d; +} pgauge; + +static void draw_bar(struct bar *b); +static void update_barbox(struct dialog *d, struct bar *b, bool buttons); +static int gauge_draw_bar_in_widget(struct dialog *d, unsigned int perc); + +static int +gauge_repaint(unsigned int perc, bool resize) +{ + perc = MIN(perc, 100); + + if (resize) { + if (dialog_size_position(&pgauge.d, HBOX, MIN_WBOX, NULL) != 0) + return (BSDDIALOG_ERROR); + if (dialog_place(&pgauge.d) != 0) + return (BSDDIALOG_ERROR); + pgauge.sized = true; + } else if (!pgauge.sized) + RETURN_ERROR("Gauge is not sized"); + + if (dialog_paint_frame(&pgauge.d) != 0) + return (BSDDIALOG_ERROR); + if (dialog_repaint_text(&pgauge.d, 0, 0, 0, HBOX) != 0) + return (BSDDIALOG_ERROR); + if (gauge_draw_bar_in_widget(&pgauge.d, perc) != 0) + return (BSDDIALOG_ERROR); + + dialog_backdrop_sync(); + if (pgauge.d.conf->shadow && pgauge.d.shadow != NULL) { + touchwin(pgauge.d.shadow); + wnoutrefresh(pgauge.d.shadow); + } + touchwin(pgauge.d.widget); + wnoutrefresh(pgauge.d.widget); + + return (BSDDIALOG_OK); +} + +static int +gauge_draw_bar_in_widget(struct dialog *d, unsigned int perc) +{ + struct bar b; + WINDOW *bbox; + + bbox = derwin(d->widget, HBOX, WBOX(d), d->h - BORDER - HBOX, + BORDER + BARPADDING); + if (bbox == NULL) + RETURN_ERROR("Cannot build gauge bar"); + + draw_borders(d->conf, bbox, RAISED); + b.win = bbox; + b.y = b.x = 1; + b.w = WBAR(d); + b.fmt = "%3d%%"; + b.perc = b.label = MIN((int)perc, 100); + b.toupdate = false; + draw_bar(&b); + delwin(bbox); + + return (BSDDIALOG_OK); +} + static void draw_bar(struct bar *b) { int barlen, xlabel; @@ -114,17 +181,88 @@ WBOX(d), RAISED); } +int +bsddialog_gauge_open(struct bsddialog_conf *conf, const char *text, int rows, + int cols, unsigned int perc) +{ + if (pgauge.active) + bsddialog_gauge_close(); + + if (prepare_dialog(conf, text, rows, cols, &pgauge.d) != 0) + return (BSDDIALOG_ERROR); + scrollok(stdscr, FALSE); + leaveok(stdscr, TRUE); + leaveok(pgauge.d.widget, TRUE); + syncok(pgauge.d.widget, TRUE); + if (pgauge.d.conf->shadow && pgauge.d.shadow != NULL) { + leaveok(pgauge.d.shadow, TRUE); + syncok(pgauge.d.shadow, TRUE); + } + pgauge.active = true; + pgauge.sized = false; + + return (gauge_repaint(perc, true)); +} + +int +bsddialog_gauge_update(const char *text, int rows, int cols, + unsigned int perc) +{ + if (!pgauge.active) + RETURN_ERROR("Gauge is not open"); + + pgauge.d.text = CHECK_STR(text); + (void)rows; + (void)cols; + + return (gauge_repaint(perc, false)); +} + +void +bsddialog_gauge_close(void) +{ + if (!pgauge.active) + return; + + if (bsddialog_inmode()) + end_dialog(&pgauge.d); + memset(&pgauge, 0, sizeof(pgauge)); +} + +void +bsddialog_gauge_flush(void) +{ + if (!pgauge.active || !pgauge.sized) + return; + + if (pgauge.d.conf->shadow && pgauge.d.shadow != NULL) { + touchwin(pgauge.d.shadow); + wnoutrefresh(pgauge.d.shadow); + } + touchwin(pgauge.d.widget); + wnoutrefresh(pgauge.d.widget); + untouchwin(stdscr); + doupdate(); +} + int bsddialog_gauge(struct bsddialog_conf *conf, const char *text, int rows, int cols, unsigned int perc, int fd, const char *sep, const char *end) { bool mainloop; - int fd2; + int fd2, retval; FILE *input; char inputbuf[2048], ntext[2048], *pntext; struct bar b; struct dialog d; + if (fd < 0) { + retval = bsddialog_gauge_open(conf, text, rows, cols, perc); + if (retval == BSDDIALOG_OK) + bsddialog_gauge_close(); + return (retval); + } + if (prepare_dialog(conf, text, rows, cols, &d) != 0) return (BSDDIALOG_ERROR); if ((b.win = newwin(1, 1, 1, 1)) == NULL) @@ -147,21 +285,19 @@ while (mainloop) { if (d.built) { hide_dialog(&d); - refresh(); /* Important for decreasing screen */ + dialog_flush(); /* Important for decreasing screen */ } if (dialog_size_position(&d, HBOX, MIN_WBOX, NULL) != 0) return (BSDDIALOG_ERROR); if (draw_dialog(&d)) return (BSDDIALOG_ERROR); - if (d.built) - refresh(); /* fix grey lines expanding screen */ TEXTPAD(&d, HBOX); update_barbox(&d, &b, false); b.w = WBAR(&d); b.perc = b.label = perc; b.toupdate = true; draw_bar(&b); - doupdate(); + dialog_flush_overlay(); if (input == NULL) /* that is fd < 0 */ break; @@ -339,7 +475,7 @@ b.toupdate = true; draw_bar(&b); - doupdate(); + dialog_flush(); /* getch(); to test with "alternate mode" */ delwin(b.win); @@ -410,7 +546,8 @@ bsddialog_total_progview / (float)(tnew - tstart); printw(pvconf->fmtbottomstr, bsddialog_total_progview, readforsec); - refresh(); + wnoutrefresh(stdscr); + dialog_flush_stdscr(); time(&told); update = false; @@ -445,16 +582,15 @@ { if (redraw) { hide_dialog(d); - refresh(); /* Important for decreasing screen */ + dialog_flush(); /* Important for decreasing screen */ } if (dialog_size_position(d, HBOX, MIN_WBOX, NULL) != 0) return (BSDDIALOG_ERROR); if (draw_dialog(d) != 0) /* doupdate() in main loop */ return (BSDDIALOG_ERROR); if (redraw) - refresh(); /* Important to fix grey lines expanding screen */ + dialog_flush(); /* Important, fix grey lines expanding screen */ TEXTPAD(d, HBOX + HBUTTONS); - b->w = WBAR(d); *bigchange = MAX(1, b->w / 10); update_barbox(d, b, true); @@ -501,9 +637,12 @@ b.label = currvalue; draw_bar(&b); } - doupdate(); - if (get_wch(&input) == ERR) + dialog_flush(); + if (get_wch(&input) == ERR) { + if (DIALOG_INPUT_HUNGUP(retval)) + break; continue; + } switch(input) { case KEY_ENTER: case 10: /* Enter */ @@ -522,6 +661,7 @@ d.bs.curr = (d.bs.curr + 1) % d.bs.nbuttons; DRAW_BUTTONS(d); break; + case KEY_BTAB: /* Shift+TAB */ case KEY_CTRL('p'): case KEY_LEFT: d.bs.curr--; @@ -580,7 +720,7 @@ default: if (shortcut_buttons(input, &d.bs)) { DRAW_BUTTONS(d); - doupdate(); + dialog_flush(); retval = BUTTONVALUE(d.bs); loop = false; } @@ -599,16 +739,15 @@ { if (redraw) { hide_dialog(d); - refresh(); /* Important for decreasing screen */ + dialog_flush(); /* Important for decreasing screen */ } if (dialog_size_position(d, HBOX, MIN_WBOX, NULL) != 0) return (BSDDIALOG_ERROR); if (draw_dialog(d) != 0) /* doupdate() in main loop */ return (BSDDIALOG_ERROR); if (redraw) - refresh(); /* Important to fix grey lines expanding screen */ + dialog_flush(); /* Important, fix grey lines expanding screen */ TEXTPAD(d, HBOX + HBUTTONS); - b->w = WBAR(d); update_barbox(d, b, true); b->toupdate = true; @@ -647,8 +786,10 @@ b.label = tout; draw_bar(&b); } - doupdate(); - if (get_wch(&input) == ERR) { /* timeout */ + dialog_flush(); + if (get_wch(&input) == ERR) { /* timeout or hangup */ + if (DIALOG_INPUT_HUNGUP(retval)) + break; tout--; if (tout < 0) { retval = BSDDIALOG_TIMEOUT; @@ -676,6 +817,7 @@ d.bs.curr = (d.bs.curr + 1) % d.bs.nbuttons; DRAW_BUTTONS(d); break; + case KEY_BTAB: /* Shift+TAB */ case KEY_LEFT: d.bs.curr--; if (d.bs.curr < 0) @@ -699,7 +841,7 @@ default: if (shortcut_buttons(input, &d.bs)) { DRAW_BUTTONS(d); - doupdate(); + dialog_flush(); retval = BUTTONVALUE(d.bs); loop = false; } diff --git a/contrib/bsddialog/lib/bsddialog.h b/contrib/bsddialog/lib/bsddialog.h --- a/contrib/bsddialog/lib/bsddialog.h +++ b/contrib/bsddialog/lib/bsddialog.h @@ -2,6 +2,7 @@ * SPDX-License-Identifier: BSD-2-Clause * * Copyright (c) 2021-2025 Alfonso Sabato Siciliano + * Copyright (c) 2026 Devin Teske * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions @@ -30,7 +31,7 @@ #include -#define LIBBSDDIALOG_VERSION "1.1" +#define LIBBSDDIALOG_VERSION "1.2" /* Return values */ #define BSDDIALOG_ERROR -1 @@ -176,6 +177,9 @@ int bsddialog_init(void); int bsddialog_init_notheme(void); +void bsddialog_keep_tite(void); +void bsddialog_fill_backdrop(void); +void bsddialog_paint_screen(void); bool bsddialog_inmode(void); int bsddialog_end(void); int bsddialog_backtitle(struct bsddialog_conf *conf, const char *backtitle); @@ -208,6 +212,20 @@ bsddialog_gauge(struct bsddialog_conf *conf, const char *text, int rows, int cols, unsigned int perc, int fd, const char *sep, const char *end); +int +bsddialog_gauge_open(struct bsddialog_conf *conf, const char *text, int rows, + int cols, unsigned int perc); + +int +bsddialog_gauge_update(const char *text, int rows, int cols, + unsigned int perc); + +void +bsddialog_gauge_close(void); + +void +bsddialog_gauge_flush(void); + int bsddialog_infobox(struct bsddialog_conf *conf, const char *text, int rows, int cols); diff --git a/contrib/bsddialog/lib/bsddialog_progressview.h b/contrib/bsddialog/lib/bsddialog_progressview.h --- a/contrib/bsddialog/lib/bsddialog_progressview.h +++ b/contrib/bsddialog/lib/bsddialog_progressview.h @@ -57,4 +57,4 @@ int cols, struct bsddialog_progviewconf *pvconf, unsigned int nminibar, struct bsddialog_fileminibar *minibar); -#endif \ No newline at end of file +#endif diff --git a/contrib/bsddialog/lib/bsddialog_theme.h b/contrib/bsddialog/lib/bsddialog_theme.h --- a/contrib/bsddialog/lib/bsddialog_theme.h +++ b/contrib/bsddialog/lib/bsddialog_theme.h @@ -2,6 +2,7 @@ * SPDX-License-Identifier: BSD-2-Clause * * Copyright (c) 2021-2025 Alfonso Sabato Siciliano + * Copyright (c) 2026 Devin Teske * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions @@ -119,6 +120,7 @@ int bsddialog_get_theme(struct bsddialog_theme *theme); bool bsddialog_hascolors(void); int bsddialog_set_default_theme(enum bsddialog_default_theme theme); +int bsddialog_set_default_theme_noflush(enum bsddialog_default_theme theme); int bsddialog_set_theme(struct bsddialog_theme *theme); -#endif \ No newline at end of file +#endif diff --git a/contrib/bsddialog/lib/datebox.c b/contrib/bsddialog/lib/datebox.c --- a/contrib/bsddialog/lib/datebox.c +++ b/contrib/bsddialog/lib/datebox.c @@ -2,6 +2,7 @@ * SPDX-License-Identifier: BSD-2-Clause * * Copyright (c) 2022-2025 Alfonso Sabato Siciliano + * Copyright (c) 2026 Devin Teske * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions @@ -251,6 +252,7 @@ if (focus) wattroff(win, t.menu.f_namecolor); + touchwin(win); wnoutrefresh(win); } @@ -261,7 +263,12 @@ int ndays, i, y, x, wd, h, w; getmaxyx(win, h, w); - wclear(win); + /* + * werase(3), not wclear(3): wclear(3) sets win->_clear and the next + * wnoutrefresh(3) forces NewScreen(3)->_clear, so doupdate(3) emits + * clear_screen (CSI-H CSI-2J) into scrollback on every key. + */ + werase(win); draw_borders(conf, win, RAISED); if (active) { wattron(win, t.dialog.arrowcolor); @@ -295,6 +302,15 @@ wnoutrefresh(win); } +static void +calendar_winopts(WINDOW *win) +{ + + leaveok(win, FALSE); + scrollok(win, FALSE); + idlok(win, FALSE); +} + static int calendar_draw(struct dialog *d, bool redraw, WINDOW *yy_win, WINDOW *mm_win, WINDOW *dd_win) @@ -303,14 +319,14 @@ if (redraw) { hide_dialog(d); - refresh(); /* Important for decreasing screen */ + dialog_flush(); /* Important for decreasing screen */ } if (dialog_size_position(d, MINHCAL, MINWCAL, NULL) != 0) return (BSDDIALOG_ERROR); if (draw_dialog(d) != 0) /* doupdate in main loop */ return (BSDDIALOG_ERROR); if (redraw) - refresh(); /* Important to fix grey lines expanding screen */ + dialog_flush(); /* Important, fix grey lines expanding screen */ TEXTPAD(d, MINHCAL + HBUTTONS); ycal = d->y + d->h - 15; @@ -321,6 +337,7 @@ update_box(d->conf, yy_win, ycal, xcal + 17, 3, 17, RAISED); update_box(d->conf, dd_win, ycal + 3, xcal, 9, 34, RAISED); wnoutrefresh(d->widget); + dialog_flush(); return (0); } @@ -348,25 +365,35 @@ if ((yy_win = newwin(1, 1, 1, 1)) == NULL) RETURN_ERROR("Cannot build WINDOW for yy"); wbkgd(yy_win, t.dialog.color); + calendar_winopts(yy_win); if ((mm_win = newwin(1, 1, 1, 1)) == NULL) RETURN_ERROR("Cannot build WINDOW for mm"); wbkgd(mm_win, t.dialog.color); + calendar_winopts(mm_win); if ((dd_win = newwin(1, 1, 1, 1)) == NULL) RETURN_ERROR("Cannot build WINDOW for dd"); wbkgd(dd_win, t.dialog.color); + calendar_winopts(dd_win); if (calendar_draw(&d, false, yy_win, mm_win, dd_win) != 0) return (BSDDIALOG_ERROR); - sel = -1; - loop = focusbuttons = true; + sel = 2; + focusbuttons = false; + d.bs.curr = conf->button.always_active ? 0 : -1; + loop = true; while (loop) { + draw_buttons(&d); + wnoutrefresh(d.widget); drawsquare(conf, mm_win, RAISED, "%15s", mm, sel == 0); drawsquare(conf, yy_win, RAISED, "%15d", yy, sel == 1); print_calendar(conf, dd_win, yy, mm, dd, sel == 2); - doupdate(); + dialog_flush_overlay(); - if (get_wch(&input) == ERR) + if (get_wch(&input) == ERR) { + if (DIALOG_INPUT_HUNGUP(retval)) + break; continue; + } switch(input) { case KEY_ENTER: case 10: /* Enter */ @@ -400,6 +427,25 @@ } DRAW_BUTTONS(d); break; + case KEY_BTAB: /* Shift+TAB */ + if (focusbuttons) { + d.bs.curr--; + if (d.bs.curr < 0) { + focusbuttons = false; + sel = 2; + d.bs.curr = conf->button.always_active ? + 0 : -1; + } + } else { + sel--; + if (sel < 0) { + focusbuttons = true; + sel = -1; + d.bs.curr = (int)d.bs.nbuttons - 1; + } + } + DRAW_BUTTONS(d); + break; case KEY_CTRL('n'): case KEY_RIGHT: if (focusbuttons) { @@ -454,7 +500,10 @@ break; case KEY_DOWN: if (focusbuttons) { - break; + sel = 2; + focusbuttons = false; + d.bs.curr = conf->button.always_active ? 0 : -1; + DRAW_BUTTONS(d); } else if (sel == 0) { datectl(DOWN_MONTH, &yy, &mm, &dd); } else if (sel == 1) { @@ -514,7 +563,7 @@ default: if (shortcut_buttons(input, &d.bs)) { DRAW_BUTTONS(d); - doupdate(); + dialog_flush(); retval = BUTTONVALUE(d.bs); loop = false; } @@ -539,14 +588,14 @@ if (redraw) { hide_dialog(d); - refresh(); /* Important for decreasing screen */ + dialog_flush(); /* Important for decreasing screen */ } if (dialog_size_position(d, 3 /*windows*/, MINWDATE, NULL) != 0) return (BSDDIALOG_ERROR); if (draw_dialog(d) != 0) return (BSDDIALOG_ERROR); if (redraw) - refresh(); /* Important to fix grey lines expanding screen */ + dialog_flush(); /* Important, fix grey lines expanding screen */ TEXTPAD(d, 3 /*windows*/ + HBUTTONS); y = d->y + d->h - 6; @@ -633,10 +682,13 @@ for (i = 0; i < 3; i++) drawsquare(conf, di[i].win, LOWERED, di[i].fmt, *di[i].value, sel == i); - doupdate(); + dialog_flush_overlay(); - if (get_wch(&input) == ERR) + if (get_wch(&input) == ERR) { + if (DIALOG_INPUT_HUNGUP(retval)) + break; continue; + } switch(input) { case KEY_ENTER: case 10: /* Enter */ @@ -672,6 +724,7 @@ } DRAW_BUTTONS(d); break; + case KEY_BTAB: /* Shift+TAB */ case KEY_CTRL('p'): case KEY_LEFT: if (focusbuttons) { @@ -706,9 +759,13 @@ break; case '+': case KEY_DOWN: - if (focusbuttons) - break; - datectl(di[sel].down, &yy, &mm, &dd); + if (focusbuttons) { + sel = 2; + focusbuttons = false; + d.bs.curr = conf->button.always_active ? 0 : -1; + DRAW_BUTTONS(d); + } else + datectl(di[sel].down, &yy, &mm, &dd); break; case KEY_F(1): if (conf->key.f1_file == NULL && @@ -727,7 +784,7 @@ default: if (shortcut_buttons(input, &d.bs)) { DRAW_BUTTONS(d); - doupdate(); + dialog_flush(); retval = BUTTONVALUE(d.bs); loop = false; } @@ -743,4 +800,4 @@ end_dialog(&d); return (retval); -} \ No newline at end of file +} diff --git a/contrib/bsddialog/lib/formbox.c b/contrib/bsddialog/lib/formbox.c --- a/contrib/bsddialog/lib/formbox.c +++ b/contrib/bsddialog/lib/formbox.c @@ -2,6 +2,7 @@ * SPDX-License-Identifier: BSD-2-Clause * * Copyright (c) 2021-2025 Alfonso Sabato Siciliano + * Copyright (c) 2026 Devin Teske * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions @@ -500,7 +501,8 @@ attron(t.form.bottomdesccolor); addstr(item->bottomdesc); attroff(t.form.bottomdesccolor); - refresh(); + wnoutrefresh(stdscr); + dialog_flush_stdscr(); } } @@ -607,7 +609,7 @@ if (redraw) { hide_dialog(d); - refresh(); /* Important for decreasing screen */ + dialog_flush(); /* Important for decreasing screen */ } f->viewrows = f->formheight; f->w = f->wmin; @@ -616,7 +618,7 @@ if (draw_dialog(d) != 0) /* doupdate() in main loop */ return (BSDDIALOG_ERROR); if (redraw) - refresh(); /* Important to fix grey lines expanding screen */ + dialog_flush(); /* Important, fix grey lines expanding screen */ TEXTPAD(d, 2 /* box borders */ + f->viewrows + HBUTTONS); update_box(d->conf, f->box, d->y + d->h - 5 - f->viewrows, d->x + 2, @@ -674,7 +676,7 @@ int cols, unsigned int formheight, unsigned int nitems, struct bsddialog_formitem *items, int *focusitem) { - bool switchfocus, changeitem, focusinform, loop; + bool switchfocus, switchfocus_last, changeitem, focusinform, loop; int next, retval, wchtype; unsigned int i; wint_t input; @@ -710,12 +712,15 @@ if (form_draw(&d, false, &form, focusinform) != 0) return (BSDDIALOG_ERROR); - changeitem = switchfocus = false; + changeitem = switchfocus = switchfocus_last = false; loop = true; while (loop) { - doupdate(); - if ((wchtype = get_wch(&input)) == ERR) + dialog_flush_overlay(); + if ((wchtype = get_wch(&input)) == ERR) { + if (DIALOG_INPUT_HUNGUP(retval)) + break; continue; + } switch(input) { case KEY_ENTER: case 10: /* Enter */ @@ -757,6 +762,27 @@ wnoutrefresh(d.widget); } break; + case KEY_BTAB: /* Shift+TAB */ + if (focusinform) { + next = previtem(form.nitems, form.pritems, + form.sel); + if (next < form.sel) + changeitem = true; + else { + switchfocus = true; + switchfocus_last = true; + } + } else { + if (d.bs.curr > 0) { + d.bs.curr--; + redrawbuttons(&d, true, true); + wnoutrefresh(d.widget); + } else if (form.sel != -1) { + switchfocus = true; + switchfocus_last = true; + } + } + break; case KEY_LEFT: if (focusinform) { if (fieldctl(item, MOVE_CURSOR_LEFT)) @@ -880,7 +906,7 @@ } else { if (shortcut_buttons(input, &d.bs)) { DRAW_BUTTONS(d); - doupdate(); + dialog_flush_widget(&d); retval = BUTTONVALUE(d.bs); loop = false; } @@ -890,7 +916,15 @@ if (switchfocus) { focusinform = !focusinform; - d.bs.curr = 0; + if (focusinform == false) { + d.bs.curr = switchfocus_last ? + (int)d.bs.nbuttons - 1 : 0; + } else { + d.bs.curr = conf->button.always_active ? 0 : -1; + next = switchfocus_last ? + lastitem(form.nitems, form.pritems) : + firstitem(form.nitems, form.pritems); + } redrawbuttons(&d, conf->button.always_active || !focusinform, !focusinform); @@ -898,13 +932,13 @@ if (focusinform == false) DRAWITEM_TRICK(&form, form.sel, false); else { - next = firstitem(form.nitems, form.pritems); if (next == form.sel) DRAWITEM_TRICK(&form, form.sel, true); else changeitem = true; } switchfocus = false; + switchfocus_last = false; } if (changeitem) { diff --git a/contrib/bsddialog/lib/lib_util.h b/contrib/bsddialog/lib/lib_util.h --- a/contrib/bsddialog/lib/lib_util.h +++ b/contrib/bsddialog/lib/lib_util.h @@ -2,6 +2,7 @@ * SPDX-License-Identifier: BSD-2-Clause * * Copyright (c) 2021-2025 Alfonso Sabato Siciliano + * Copyright (c) 2026 Devin Teske * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions @@ -76,8 +77,16 @@ #define LARROW(c) (c->ascii_lines ? '<' : ACS_LARROW) #define RARROW(c) (c->ascii_lines ? '>' : ACS_RARROW) #define DRAW_BUTTONS(d) do { \ - draw_buttons(&d); \ - wnoutrefresh(d.widget); \ + draw_buttons(&(d)); \ + wnoutrefresh((d).widget); \ +} while (0) +#define BUTTONS_TAB_FORWARD(bs) do { \ + (bs).curr = ((bs).curr + 1) % (int)(bs).nbuttons; \ +} while (0) +#define BUTTONS_TAB_BACKWARD(bs) do { \ + (bs).curr--; \ + if ((bs).curr < 0) \ + (bs).curr = (int)(bs).nbuttons - 1; \ } while (0) /* internal types */ @@ -127,7 +136,27 @@ bool shortcut_buttons(wint_t key, struct buttons *bs); /* widget utils */ +/* + * After get_wch(3) == ERR: true if stdin is hung up / at EOF. Sets *retval + * to BSDDIALOG_ESC. Caller must break/leave the input loop on true. + */ +#define DIALOG_INPUT_HUNGUP(retval) \ + (dialog_stdin_hungup() ? ((retval) = BSDDIALOG_ESC, true) : false) +bool dialog_stdin_hungup(void); int hide_dialog(struct dialog *d); +void dialog_flush_reset(void); +void dialog_backdrop_sync(void); +void dialog_flush(void); +void dialog_flush_widget(struct dialog *d); +void dialog_flush_overlay(void); +void dialog_flush_stdscr(void); +void dialog_backtitle_set(struct bsddialog_conf *conf, const char *backtitle); +void dialog_backtitle_paint(void); +void dialog_backtitle_clear(void); +void paint_screen(chtype attr); +void paint_screen_backdrop(void); +void bsddialog_cancel_alternate_screen(void); +void bsddialog_enter_alternate_screen(void); int f1help_dialog(struct bsddialog_conf *conf); void @@ -140,6 +169,10 @@ void rtextpad(struct dialog *d, int ytext, int xtext, int upnotext, int downnotext); +void +textpad_to_widget(struct dialog *d, int ytext, int xtext, int upnotext, + int downnotext); + /* (auto) sizing and (auto) position */ int set_widget_size(struct bsddialog_conf *conf, int rows, int cols, int *h, @@ -160,6 +193,11 @@ /* dialog */ void end_dialog(struct dialog *d); int draw_dialog(struct dialog *d); +int dialog_repaint_frame(struct dialog *d); +int dialog_place(struct dialog *d); +int dialog_paint_frame(struct dialog *d); +int dialog_repaint_text(struct dialog *d, int ytext, int xtext, int upnotext, + int downnotext); int prepare_dialog(struct bsddialog_conf *conf, const char *text, int rows, diff --git a/contrib/bsddialog/lib/lib_util.c b/contrib/bsddialog/lib/lib_util.c --- a/contrib/bsddialog/lib/lib_util.c +++ b/contrib/bsddialog/lib/lib_util.c @@ -2,6 +2,7 @@ * SPDX-License-Identifier: BSD-2-Clause * * Copyright (c) 2021-2025 Alfonso Sabato Siciliano + * Copyright (c) 2026 Devin Teske * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions @@ -25,8 +26,13 @@ * SUCH DAMAGE. */ +#include +#include + #include #include +#include +#include #include #include #include @@ -36,6 +42,11 @@ #include "bsddialog_theme.h" #include "lib_util.h" +#include +#ifdef buttons +#undef buttons +#endif + /* * -1- Error and diagnostic * @@ -80,6 +91,7 @@ * ---------------------------------------------------- * -5- (Dialog) Widget components and utils * + * dialog_stdin_hungup(); * hide_dialog(struct dialog); * f1help_dialog(conf); * draw_borders(conf, win, elev); @@ -937,6 +949,238 @@ /* * -5- Widget components and utilities */ +static chtype +screen_backdrop(void) +{ + + return (t.screen.color); +} + +void +paint_screen(chtype attr) +{ + int col, row; + + bkgd(attr); + for (row = 0; row < SCREENLINES; row++) { + for (col = 0; col < SCREENCOLS; col++) + mvwaddch(stdscr, row, col, ' ' | attr); + } +} + +void +paint_screen_backdrop(void) +{ + + paint_screen(screen_backdrop()); +} + +void +bsddialog_cancel_alternate_screen(void) +{ + /* + * Leave alternate-screen buffer without refresh(3) or clear_screen. + * refresh(3) here would push a blank frame into scrollback; + * clear_screen would flush full-screen repaint before gauge exists. + */ + if ((!isatty(fileno(stdout)) && !isatty(fileno(stderr))) || + exit_ca_mode == NULL || exit_ca_mode[0] == '\0') + return; + + (void)putp(exit_ca_mode); + enter_ca_mode = NULL; + exit_ca_mode = NULL; +} + +void +bsddialog_enter_alternate_screen(void) +{ + + /* + * Draw on the alternate-screen buffer so ncurses home+clear sequences + * (CSI-H CSI-2J) never append to scrollback. bsdpv(3) uses + * bsddialog_init_notheme(3) and optionally bsddialog_keep_tite(3) + * instead. + */ + if ((!isatty(fileno(stdout)) && !isatty(fileno(stderr))) || + enter_ca_mode == NULL || enter_ca_mode[0] == '\0') + return; + + (void)putp(enter_ca_mode); +} + +static bool backdrop_synced; + +/* + * Backtitle is drawn on stdscr(3). dialog_backdrop_sync(3) fills the whole + * screen after bsddialog_backtitle(3) runs (utility order), so remember the + * text and repaint it after every full backdrop fill. + */ +static char *backtitle_text; +static bool backtitle_ascii_lines; +static bool backtitle_no_lines; +static bool backtitle_active; + +void +dialog_backtitle_clear(void) +{ + + free(backtitle_text); + backtitle_text = NULL; + backtitle_ascii_lines = false; + backtitle_no_lines = false; + backtitle_active = false; +} + +void +dialog_backtitle_set(struct bsddialog_conf *conf, const char *backtitle) +{ + + dialog_backtitle_clear(); + if (conf == NULL || backtitle == NULL) + return; + backtitle_text = strdup(backtitle); + if (backtitle_text == NULL) + return; + backtitle_ascii_lines = conf->ascii_lines; + backtitle_no_lines = conf->no_lines; + backtitle_active = true; +} + +void +dialog_backtitle_paint(void) +{ + + if (!backtitle_active || backtitle_text == NULL) + return; + + move(0, 1); + clrtoeol(); + addstr(backtitle_text); + if (backtitle_no_lines != true) { + if (backtitle_ascii_lines) + mvhline(1, 1, '-', SCREENCOLS - 2); + else + mvhline_set(1, 1, WACS_HLINE, SCREENCOLS - 2); + } +} + +void +dialog_backdrop_sync(void) +{ + + if (backdrop_synced) + return; + /* + * bkgd(3) at theme init only sets the attribute; + * paint_screen_backdrop(3) fills stdscr(3) in memory. wnoutrefresh(3) + * stdscr(3) must run before any other wnoutrefresh(3) in the same + * frame; otherwise the sparse stdscr merge wipes dialog windows + * already queued (calendar(3) would lose box while the blue backdrop + * returned). + * + * Full-screen fill also erases any prior bsddialog_backtitle(3); paint + * the remembered backtitle back onto the top lines before refresh. + */ + paint_screen_backdrop(); + dialog_backtitle_paint(); + wnoutrefresh(stdscr); + /* + * Leave the backdrop in newscr(3) but do not keep stdscr(3) touched: + * bkgd(3) on a touched stdscr(3) forces clear_screen into scrollback + * on the next doupdate(3) (see bsddialog_fill_backdrop(3)). + */ + untouchwin(stdscr); + backdrop_synced = true; +} + +void +dialog_flush_reset(void) +{ + + backdrop_synced = false; +} + +void +dialog_flush(void) +{ + + /* + * Match bsddialog_gauge_flush(3): untouchwin(3) stdscr(3) before + * doupdate(3) so bkgd(3) does not force clear_screen (CSI-H CSI-2J) + * into scrollback. Sync backdrop with dialog_backdrop_sync(3) from + * draw_dialog(3) before queueing widget damage. + */ + untouchwin(stdscr); + doupdate(); + untouchwin(stdscr); +} + +void +dialog_flush_widget(struct dialog *d) +{ + + if (d->conf->shadow && d->shadow != NULL) { + touchwin(d->shadow); + wnoutrefresh(d->shadow); + } + touchwin(d->widget); + wnoutrefresh(d->widget); + dialog_flush(); +} + +void +dialog_flush_overlay(void) +{ + + /* + * Flush after wnoutrefresh(3) on separate newwin(3) layers placed above + * d->widget (calendar day grid, menu box, slider controls, ...). Do not + * wnoutrefresh(3) the widget here (that merge would paint over them). + */ + dialog_flush(); +} + +void +dialog_flush_stdscr(void) +{ + + /* + * Flush after an explicit wnoutrefresh(3) on stdscr(3) (backtitle, + * bottom description). Do not untouchwin(3) before doupdate(3). + */ + doupdate(); + if (backdrop_synced) + untouchwin(stdscr); +} + +/* + * True when stdin is hung up or at EOF: poll(2) reports ready/hangup but + * FIONREAD is 0. That is what happens when the hosting terminal (e.g. + * xterm -e) is closed while a dialog is still in its get_wch(3) loop; + * read(2) returns 0 forever and a bare "continue" on ERR spins one CPU. + */ +bool +dialog_stdin_hungup(void) +{ + struct pollfd pfd; + int avail; + + pfd.fd = STDIN_FILENO; + pfd.events = POLLIN | POLLHUP | POLLERR; + pfd.revents = 0; + if (poll(&pfd, 1, 0) <= 0) + return (false); + if (pfd.revents & (POLLHUP | POLLERR)) + return (true); + if ((pfd.revents & POLLIN) == 0) + return (false); + avail = 0; + if (ioctl(STDIN_FILENO, FIONREAD, &avail) == 0 && avail == 0) + return (true); + return (false); +} + int hide_dialog(struct dialog *d) { WINDOW *clear; @@ -1028,7 +1272,9 @@ update_box(struct bsddialog_conf *conf, WINDOW *win, int y, int x, int h, int w, enum elevation elev) { - wclear(win); + /* werase(3): see print_calendar(3) in datebox.c + * (wclear(3) floods scrollback) */ + werase(win); wresize(win, h, w); mvwin(win, y, x); draw_borders(conf, win, elev); @@ -1037,11 +1283,46 @@ void rtextpad(struct dialog *d, int ytext, int xtext, int upnotext, int downnotext) { + int dmaxcol, dmincol; + + dmincol = BORDER + TEXTHMARGIN; + dmaxcol = dmincol + getmaxx(d->textpad) - xtext - 1; pnoutrefresh(d->textpad, ytext, xtext, d->y + BORDER + upnotext, - d->x + BORDER + TEXTHMARGIN, + d->x + dmincol, d->y + d->h - 1 - downnotext - BORDER, - d->x + d->w - TEXTHMARGIN - BORDER); + d->x + dmaxcol); +} + +void +textpad_to_widget(struct dialog *d, int ytext, int xtext, int upnotext, + int downnotext) +{ + int dmaxcol, dmaxrow, dmincol, dminrow, row; + + dminrow = BORDER + upnotext; + dmincol = BORDER + TEXTHMARGIN; + dmaxrow = d->h - 1 - downnotext - BORDER; + /* + * Match the textpad width (d->w - BORDERS - TEXTHMARGINS). Using + * d->w - TEXTHMARGIN - BORDER reads one source column past the pad; + * copywin(3) then pulls garbage/reverse-video onto the widget row. + */ + dmaxcol = dmincol + getmaxx(d->textpad) - xtext - 1; + + /* + * Erase the widget text band before copywin(3). Overlay FALSE skips + * blank source cells; without this, reverse-video residue can remain + * when a prompt line shrinks (e.g. unbounded read-count vs pct bar). + */ + wattrset(d->widget, t.dialog.color); + for (row = dminrow; row <= dmaxrow; row++) + mvwhline(d->widget, row, dmincol, ' ', + dmaxcol - dmincol + 1); + + (void)copywin(d->textpad, d->widget, ytext, xtext, dminrow, dmincol, + dmaxrow, dmaxcol, false); + wattrset(d->widget, t.dialog.color); } /* @@ -1081,14 +1362,13 @@ switch (wtext[2]) { case L'n': - wattron(win, t.dialog.color); - wattrset(win, A_NORMAL); + wattrset(win, t.dialog.color); break; case L'b': - wattron(win, A_BOLD); + wattrset(win, t.dialog.color | A_BOLD); break; case L'B': - wattroff(win, A_BOLD); + wattrset(win, t.dialog.color); break; case L'd': wattron(win, A_DIM); @@ -1213,6 +1493,17 @@ loop = false; break; case L'\n': + /* + * When the line already fills the pad width (x == cols), + * wmove(3)/wclrtoeol(3) would touch column cols (past the + * last valid cell) and leave a reverse-video blank that + * copywin(3) later copies onto the widget. + */ + if (x < cols) { + wmove(pad, y, x); + wattrset(pad, t.dialog.color); + wclrtoeol(pad); + } x = 0; y++; j = -1; @@ -1228,6 +1519,8 @@ j = -1; break; case L' ': + wmove(pad, y, x); + mvwaddwch(pad, y, x, L' '); x++; if (x >= cols) { x = 0; @@ -1245,20 +1538,81 @@ i++; } + wattrset(pad, t.dialog.color); + free(wtext); free(string); return (0); } -int draw_dialog(struct dialog *d) +static void +paint_shadow_lshape(WINDOW *win, int h, int w) +{ + int col, row, x0, y0; + + if (win == NULL || h <= 0 || w <= 0) + return; + + /* + * The shadow window is the same size as the widget but offset + * down/right by t.shadow.y/t.shadow.x. Only the bottom band and right + * band of this window fall outside the widget on screen; the rest + * overlaps widget interior. Leave those cells blank so wnoutrefresh(3) + * does not paint black over the widget (e.g. a stray cell after full- + * width prompt). + */ + y0 = h - (int)t.shadow.y; + if (y0 < 0) + y0 = 0; + x0 = w - (int)t.shadow.x; + if (x0 < 0) + x0 = 0; + + /* + * werase(3), not wclear(3): wclear(3) sets win->_clear and the next + * wnoutrefresh(3) forces NewScreen(3)->_clear, so doupdate(3) emits + * clear_screen (CSI-H CSI-2J); a full blue backdrop page in scrollback + * before dialog is painted. + */ + werase(win); + for (row = y0; row < h; row++) { + for (col = 0; col < w; col++) + mvwaddch(win, row, col, ' ' | t.shadow.color); + } + for (row = 0; row < h; row++) { + for (col = x0; col < w; col++) + mvwaddch(win, row, col, ' ' | t.shadow.color); + } +} + +int +dialog_place(struct dialog *d) +{ + + if (d->conf->shadow) { + wresize(d->shadow, d->h, d->w); + mvwin(d->shadow, d->y + t.shadow.y, d->x + t.shadow.x); + wbkgd(d->shadow, t.shadow.color); + paint_shadow_lshape(d->shadow, d->h, d->w); + } + + wresize(d->widget, d->h, d->w); + mvwin(d->widget, d->y, d->x); + wbkgd(d->widget, t.dialog.color); + + return (0); +} + +int +dialog_paint_frame(struct dialog *d) { int wtitle, wbottomtitle; cchar_t ts, ltee, rtee; if (d->conf->ascii_lines) { setcchar(&ts, L"-", 0, 0, NULL); - setcchar(<ee, L"+", 0, 0,NULL); + setcchar(<ee, L"+", 0, 0, NULL); setcchar(&rtee, L"+", 0, 0, NULL); } else { ts = *WACS_HLINE; @@ -1266,16 +1620,7 @@ rtee = *WACS_RTEE; } - if (d->conf->shadow) { - wclear(d->shadow); - wresize(d->shadow, d->h, d->w); - mvwin(d->shadow, d->y + t.shadow.y, d->x + t.shadow.x); - wnoutrefresh(d->shadow); - } - - wclear(d->widget); - wresize(d->widget, d->h, d->w); - mvwin(d->widget, d->y, d->x); + werase(d->widget); draw_borders(d->conf, d->widget, RAISED); if (d->conf->title != NULL) { @@ -1321,9 +1666,47 @@ wattroff(d->widget, t.dialog.bottomtitlecolor); } + return (0); +} + +int +dialog_repaint_frame(struct dialog *d) +{ + + if (dialog_place(d) != 0) + return (BSDDIALOG_ERROR); + return (dialog_paint_frame(d)); +} + +int +dialog_repaint_text(struct dialog *d, int ytext, int xtext, int upnotext, + int downnotext) +{ + + werase(d->textpad); + wresize(d->textpad, 1, d->w - BORDERS - TEXTHMARGINS); + if (print_textpad(d->conf, d->textpad, d->text) != 0) + return (BSDDIALOG_ERROR); + textpad_to_widget(d, ytext, xtext, upnotext, downnotext); + d->built = true; + + return (0); +} + +int draw_dialog(struct dialog *d) +{ + if (dialog_repaint_frame(d) != 0) + return (BSDDIALOG_ERROR); + /* + * Sync the backdrop after painting, immediately before wnoutrefresh(3) + * on dialog windows (same order as bsddialog_gauge_draw(3)). + */ + dialog_backdrop_sync(); + if (d->conf->shadow) + wnoutrefresh(d->shadow); wnoutrefresh(d->widget); - wclear(d->textpad); + werase(d->textpad); /* `infobox "" 0 2` fails but text is empty and textpad remains 1 1 */ wresize(d->textpad, 1, d->w - BORDERS - TEXTHMARGINS); @@ -1357,6 +1740,8 @@ if ((d->widget = newwin(1, 1, 1, 1)) == NULL) RETURN_ERROR("Cannot build WINDOW widget"); wbkgd(d->widget, t.dialog.color); + scrollok(d->widget, FALSE); + leaveok(d->widget, TRUE); /* fake for textpad */ if ((d->textpad = newpad(1, 1)) == NULL) @@ -1364,4 +1749,4 @@ wbkgd(d->textpad, t.dialog.color); return (0); -} \ No newline at end of file +} diff --git a/contrib/bsddialog/lib/libbsddialog.c b/contrib/bsddialog/lib/libbsddialog.c --- a/contrib/bsddialog/lib/libbsddialog.c +++ b/contrib/bsddialog/lib/libbsddialog.c @@ -2,6 +2,7 @@ * SPDX-License-Identifier: BSD-2-Clause * * Copyright (c) 2021-2025 Alfonso Sabato Siciliano + * Copyright (c) 2026 Devin Teske * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions @@ -26,6 +27,7 @@ */ #include +#include #include #include "bsddialog.h" @@ -51,6 +53,9 @@ error += cbreak(); error += noecho(); curs_set(0); + clearok(stdscr, FALSE); + scrollok(stdscr, FALSE); + leaveok(stdscr, TRUE); if (error != OK) { bsddialog_end(); RETURN_ERROR("Cannot init curses (keypad and cursor)"); @@ -87,12 +92,14 @@ in_bsddialog_mode = false; return (BSDDIALOG_ERROR); } + bsddialog_enter_alternate_screen(); return (BSDDIALOG_OK); } int bsddialog_end(void) { + dialog_backtitle_clear(); if (endwin() != OK) RETURN_ERROR("Cannot end curses (endwin)"); in_bsddialog_mode = false; @@ -100,20 +107,46 @@ return (BSDDIALOG_OK); } +void +bsddialog_keep_tite(void) +{ + + bsddialog_cancel_alternate_screen(); +} + +void +bsddialog_fill_backdrop(void) +{ + + paint_screen_backdrop(); + dialog_backtitle_paint(); + /* + * dlg_clear(3) fills stdscr(3) in memory only. Without untouchwin(3) + * the next refresh(3) would flush a blue screen before the gauge is + * copied into stdscr(3). + */ + untouchwin(stdscr); +} + +void +bsddialog_paint_screen(void) +{ + + dialog_backdrop_sync(); + dialog_flush(); +} + int bsddialog_backtitle(struct bsddialog_conf *conf, const char *backtitle) { CHECK_PTR(conf); - move(0, 1); - clrtoeol(); - addstr(CHECK_STR(backtitle)); - if (conf->no_lines != true) { - if (conf->ascii_lines) - mvhline(1, 1, '-', SCREENCOLS - 2); - else - mvhline_set(1, 1, WACS_HLINE, SCREENCOLS - 2); - } - + /* + * Remember text for dialog_backdrop_sync(3): the utility draws the + * backtitle first, then draw_dialog(3) fills stdscr(3) with the themed + * backdrop and would otherwise erase these lines. + */ + dialog_backtitle_set(conf, CHECK_STR(backtitle)); + dialog_backtitle_paint(); wnoutrefresh(stdscr); return (BSDDIALOG_OK); @@ -124,7 +157,7 @@ int rv; rv = bsddialog_backtitle(conf, backtitle); - doupdate(); + dialog_flush_stdscr(); return (rv); } @@ -162,4 +195,4 @@ move(y, 0); clrtobot(); refresh(); -} \ No newline at end of file +} diff --git a/contrib/bsddialog/lib/menubox.c b/contrib/bsddialog/lib/menubox.c --- a/contrib/bsddialog/lib/menubox.c +++ b/contrib/bsddialog/lib/menubox.c @@ -2,6 +2,7 @@ * SPDX-License-Identifier: BSD-2-Clause * * Copyright (c) 2021-2025 Alfonso Sabato Siciliano + * Copyright (c) 2026 Devin Teske * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions @@ -392,6 +393,7 @@ addstr(pritem->bottomdesc); attroff(t.menu.bottomdesccolor); wnoutrefresh(stdscr); + dialog_flush_stdscr(); } } } @@ -458,7 +460,7 @@ { if (redraw) { hide_dialog(d); - refresh(); /* Important for decreasing screen */ + dialog_flush(); /* Important for decreasing screen */ } m->menurows = m->apimenurows; if (menu_size_position(d, m) != 0) @@ -466,7 +468,7 @@ if (draw_dialog(d) != 0) /* doupdate() in main loop */ return (BSDDIALOG_ERROR); if (redraw) - refresh(); /* Important to fix grey lines expanding screen */ + dialog_flush(); /* Important, fix grey lines expanding screen */ TEXTPAD(d, 2/*bmenu*/ + m->menurows + HBUTTONS); /* selected item in view*/ @@ -538,9 +540,12 @@ changeitem = false; loop = true; while (loop) { - doupdate(); - if (get_wch(&input) == ERR) + dialog_flush_overlay(); + if (get_wch(&input) == ERR) { + if (DIALOG_INPUT_HUNGUP(retval)) + break; continue; + } switch(input) { case KEY_ENTER: case 10: /* Enter */ @@ -563,6 +568,7 @@ d.bs.curr = (d.bs.curr + 1) % d.bs.nbuttons; DRAW_BUTTONS(d); break; + case KEY_BTAB: /* Shift+TAB */ case KEY_LEFT: d.bs.curr--; if (d.bs.curr < 0) @@ -642,7 +648,7 @@ if (conf->menu.shortcut_buttons) { if (shortcut_buttons(input, &d.bs)) { DRAW_BUTTONS(d); - doupdate(); + dialog_flush(); retval = BUTTONVALUE(d.bs); if (m.pritems[m.sel].type == MENUMODE) m.pritems[m.sel].on = true; diff --git a/contrib/bsddialog/lib/messagebox.c b/contrib/bsddialog/lib/messagebox.c --- a/contrib/bsddialog/lib/messagebox.c +++ b/contrib/bsddialog/lib/messagebox.c @@ -2,6 +2,7 @@ * SPDX-License-Identifier: BSD-2-Clause * * Copyright (c) 2021-2025 Alfonso Sabato Siciliano + * Copyright (c) 2026 Devin Teske * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions @@ -75,14 +76,14 @@ if (redraw) { /* redraw: RESIZE or F1 */ hide_dialog(d); - refresh(); /* Important for decreasing screen */ + dialog_flush(); /* Important for decreasing screen */ } if (message_size_position(d, &s->htext) != 0) return (BSDDIALOG_ERROR); if (draw_dialog(d) != 0) /* doupdate() in main loop */ return (BSDDIALOG_ERROR); if (redraw) - refresh(); /* Important to fix grey lines expanding screen */ + dialog_flush(); /* Important, fix grey lines expanding screen */ s->printrows = d->h - BORDER - HBUTTONS - BORDER; s->ypad = 0; @@ -112,9 +113,12 @@ loop = true; while (loop) { textupdate(&d, &s); - doupdate(); - if (get_wch(&input) == ERR) + dialog_flush_overlay(); + if (get_wch(&input) == ERR) { + if (DIALOG_INPUT_HUNGUP(retval)) + break; continue; + } switch (input) { case KEY_ENTER: case 10: /* Enter */ @@ -132,6 +136,7 @@ d.bs.curr = (d.bs.curr + 1) % d.bs.nbuttons; DRAW_BUTTONS(d); break; + case KEY_BTAB: /* Shift+TAB */ case KEY_LEFT: d.bs.curr--; if (d.bs.curr < 0) @@ -181,7 +186,7 @@ default: if (shortcut_buttons(input, &d.bs)) { DRAW_BUTTONS(d); - doupdate(); + dialog_flush_overlay(); retval = BUTTONVALUE(d.bs); loop = false; } @@ -223,7 +228,7 @@ if (draw_dialog(&d) != 0) return (BSDDIALOG_ERROR); TEXTPAD(&d, 0); - doupdate(); + dialog_flush_overlay(); end_dialog(&d); diff --git a/contrib/bsddialog/lib/slider.c b/contrib/bsddialog/lib/slider.c --- a/contrib/bsddialog/lib/slider.c +++ b/contrib/bsddialog/lib/slider.c @@ -2,6 +2,7 @@ * SPDX-License-Identifier: BSD-2-Clause * * Copyright (c) 2025 Braulio Rivas + * Copyright (c) 2026 Devin Teske * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions @@ -283,7 +284,9 @@ chtype ch; getmaxyx(win, height, width); - wclear(win); + /* werase(3): see print_calendar(3) in datebox.c + * (wclear(3) floods scrollback) */ + werase(win); draw_borders(conf, win, RAISED); if (active) { @@ -335,14 +338,14 @@ if (redraw) { hide_dialog(d); - refresh(); /* Important for decreasing screen */ + dialog_flush(); /* Important for decreasing screen */ } if (dialog_size_position(d, MINHSLIDER, MINWSLIDER, NULL) != 0) return (BSDDIALOG_ERROR); if (draw_dialog(d) != 0) /* doupdate in main loop */ return (BSDDIALOG_ERROR); if (redraw) - refresh(); /* Important to fix grey lines expanding screen */ + dialog_flush(); /* Important, fix grey lines expanding screen */ TEXTPAD(d, MINHSLIDER + HBUTTONS); yslider = d->y + d->h - 15; @@ -433,10 +436,13 @@ drawsquare(conf, step_win, RAISED, sel == STEP_WIN, "%15d", ctl.step); print_slider(conf, slider_win, blocks, nblocks, length, start, end, sel == SLIDER_WIN); - doupdate(); + dialog_flush_overlay(); - if (get_wch(&input) == ERR) + if (get_wch(&input) == ERR) { + if (DIALOG_INPUT_HUNGUP(retval)) + break; continue; + } switch (input) { case KEY_ENTER: case 10: /* Enter */ @@ -470,6 +476,24 @@ } DRAW_BUTTONS(dialog); break; + case KEY_BTAB: /* Shift+TAB */ + if (focusbuttons) { + dialog.bs.curr--; + if (dialog.bs.curr < 0) { + focusbuttons = false; + sel = SLIDER_WIN; + dialog.bs.curr = + conf->button.always_active ? 0 : -1; + } + } else if (sel == START_WIN) { + focusbuttons = true; + sel = NULLWIN; + dialog.bs.curr = (int)dialog.bs.nbuttons - 1; + } else { + sel--; + } + DRAW_BUTTONS(dialog); + break; case KEY_CTRL('n'): case KEY_RIGHT: if (focusbuttons) { @@ -653,7 +677,7 @@ default: if (shortcut_buttons(input, &dialog.bs)) { DRAW_BUTTONS(dialog); - doupdate(); + dialog_flush(); retval = BUTTONVALUE(dialog.bs); loop = false; } diff --git a/contrib/bsddialog/lib/textbox.c b/contrib/bsddialog/lib/textbox.c --- a/contrib/bsddialog/lib/textbox.c +++ b/contrib/bsddialog/lib/textbox.c @@ -2,6 +2,7 @@ * SPDX-License-Identifier: BSD-2-Clause * * Copyright (c) 2021-2025 Alfonso Sabato Siciliano + * Copyright (c) 2026 Devin Teske * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions @@ -105,14 +106,14 @@ { if (redraw) { hide_dialog(d); - refresh(); /* Important for decreasing screen */ + dialog_flush(); /* Important for decreasing screen */ } if (textbox_size_position(d, st) != 0) return (BSDDIALOG_ERROR); if (draw_dialog(d) != 0) /* wrefresh() and prefresh() in main loop */ return (BSDDIALOG_ERROR); if (redraw) - refresh(); /* Important to fix grey lines expanding screen */ + dialog_flush(); /* Important, fix grey lines expanding screen */ st->ys = d->y + 1; st->xs = (st->margin == 0) ? d->x + 1 : d->x + 2; @@ -185,15 +186,18 @@ * Trick, overflow multicolumn charchter right border: * wnoutrefresh(widget); * pnoutrefresh(pad, ypad, xpad, ys, xs, ye, xe); - * doupdate(); + * dialog_flush(); */ wrefresh(d.widget); prefresh(st.pad, st.ypad, st.xpad, st.ys, st.xs, st.ye, st.xe); - if (get_wch(&input) == ERR) + if (get_wch(&input) == ERR) { + if (DIALOG_INPUT_HUNGUP(retval)) + break; continue; + } if (shortcut_buttons(input, &d.bs)) { DRAW_BUTTONS(d); - doupdate(); + dialog_flush_widget(&d); retval = BUTTONVALUE(d.bs); break; /* loop */ } @@ -210,7 +214,11 @@ } break; case '\t': /* TAB */ - d.bs.curr = (d.bs.curr + 1) % d.bs.nbuttons; + BUTTONS_TAB_FORWARD(d.bs); + DRAW_BUTTONS(d); + break; + case KEY_BTAB: /* Shift+TAB */ + BUTTONS_TAB_BACKWARD(d.bs); DRAW_BUTTONS(d); break; case KEY_HOME: diff --git a/contrib/bsddialog/lib/theme.c b/contrib/bsddialog/lib/theme.c --- a/contrib/bsddialog/lib/theme.c +++ b/contrib/bsddialog/lib/theme.c @@ -2,6 +2,7 @@ * SPDX-License-Identifier: BSD-2-Clause * * Copyright (c) 2021-2025 Alfonso Sabato Siciliano + * Copyright (c) 2026 Devin Teske * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions @@ -152,7 +153,7 @@ }; static void -set_theme(struct bsddialog_theme *dst, struct bsddialog_theme *src) +copy_theme(struct bsddialog_theme *dst, struct bsddialog_theme *src) { dst->screen.color = src->screen.color; @@ -200,7 +201,13 @@ dst->button.color = src->button.color; dst->button.f_shortcutcolor = src->button.f_shortcutcolor; dst->button.shortcutcolor = src->button.shortcutcolor; +} + +static void +set_theme(struct bsddialog_theme *dst, struct bsddialog_theme *src) +{ + copy_theme(dst, src); bkgd(dst->screen.color); } @@ -208,7 +215,7 @@ int bsddialog_get_theme(struct bsddialog_theme *theme) { CHECK_PTR(theme); - set_theme(theme, &t); + copy_theme(theme, &t); return (BSDDIALOG_OK); } @@ -217,13 +224,15 @@ { CHECK_PTR(theme); set_theme(&t, theme); - wnoutrefresh(stdscr); + dialog_flush_reset(); return (BSDDIALOG_OK); } -int bsddialog_set_default_theme(enum bsddialog_default_theme newtheme) +static int +install_default_theme(enum bsddialog_default_theme newtheme) { + if (newtheme == BSDDIALOG_THEME_3D) { set_theme(&t, &flat); t.dialog.lineraisecolor = @@ -239,7 +248,27 @@ "to use enum bsddialog_default_theme", newtheme); } - wnoutrefresh(stdscr); + + return (BSDDIALOG_OK); +} + +int bsddialog_set_default_theme(enum bsddialog_default_theme newtheme) +{ + + if (install_default_theme(newtheme) != BSDDIALOG_OK) + return (BSDDIALOG_ERROR); + dialog_flush_reset(); + + return (BSDDIALOG_OK); +} + +int +bsddialog_set_default_theme_noflush(enum bsddialog_default_theme newtheme) +{ + + if (install_default_theme(newtheme) != BSDDIALOG_OK) + return (BSDDIALOG_ERROR); + dialog_flush_reset(); return (BSDDIALOG_OK); } diff --git a/contrib/bsddialog/lib/timebox.c b/contrib/bsddialog/lib/timebox.c --- a/contrib/bsddialog/lib/timebox.c +++ b/contrib/bsddialog/lib/timebox.c @@ -2,6 +2,7 @@ * SPDX-License-Identifier: BSD-2-Clause * * Copyright (c) 2021-2025 Alfonso Sabato Siciliano + * Copyright (c) 2026 Devin Teske * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions @@ -68,14 +69,14 @@ if (redraw) { hide_dialog(d); - refresh(); /* Important for decreasing screen */ + dialog_flush(); /* Important for decreasing screen */ } if (dialog_size_position(d, HBOX, MINWTIME, NULL) != 0) return (BSDDIALOG_ERROR); if (draw_dialog(d) != 0) /* doupdate() in mail loop */ return (BSDDIALOG_ERROR); if (redraw) - refresh(); /* Important to fix grey lines expanding screen */ + dialog_flush(); /* Important, fix grey lines expanding screen */ TEXTPAD(d, HBOX + HBUTTONS); y = d->y + d->h - BORDER - HBUTTONS - HBOX; @@ -125,9 +126,12 @@ while (loop) { for (i = 0; i < 3; i++) drawsquare(conf, c[i].win, c[i].value, sel == i); - doupdate(); - if (get_wch(&input) == ERR) + dialog_flush_overlay(); + if (get_wch(&input) == ERR) { + if (DIALOG_INPUT_HUNGUP(retval)) + break; continue; + } switch(input) { case KEY_ENTER: case 10: /* Enter */ @@ -163,6 +167,7 @@ } DRAW_BUTTONS(d); break; + case KEY_BTAB: /* Shift+TAB */ case KEY_CTRL('p'): case KEY_LEFT: if (focusbuttons) { @@ -199,10 +204,15 @@ break; case '+': case KEY_DOWN: - if (focusbuttons) - break; - c[sel].value = c[sel].value < c[sel].max ? - c[sel].value + 1 : 0; + if (focusbuttons) { + sel = 2; + focusbuttons = false; + d.bs.curr = conf->button.always_active ? 0 : -1; + DRAW_BUTTONS(d); + } else { + c[sel].value = c[sel].value < c[sel].max ? + c[sel].value + 1 : 0; + } break; case KEY_F(1): if (conf->key.f1_file == NULL && @@ -221,7 +231,7 @@ default: if (shortcut_buttons(input, &d.bs)) { DRAW_BUTTONS(d); - doupdate(); + dialog_flush(); retval = BUTTONVALUE(d.bs); loop = false; }