xref: /freebsd/contrib/bsddialog/lib/timebox.c (revision af23369a6deaaeb612ab266eb88b8bb8d560c322)
1 /*-
2  * SPDX-License-Identifier: BSD-2-Clause
3  *
4  * Copyright (c) 2021-2022 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 <sys/param.h>
29 
30 #include <ctype.h>
31 #include <curses.h>
32 #include <string.h>
33 
34 #include "bsddialog.h"
35 #include "bsddialog_theme.h"
36 #include "lib_util.h"
37 
38 #define MINWDATE   23 /* 3 windows and their borders */
39 #define MINWTIME   14 /* 3 windows and their borders */
40 
41 static void
42 drawsquare(struct bsddialog_conf *conf, WINDOW *win, const char *fmt,
43     const void *value, bool focus)
44 {
45 	int h, l, w;
46 
47 	getmaxyx(win, h, w);
48 	draw_borders(conf, win, h, w, LOWERED);
49 	if (focus) {
50 		l = 2 + w%2;
51 		wattron(win, t.dialog.arrowcolor);
52 		mvwhline(win, 0, w/2 - l/2,
53 		    conf->ascii_lines ? '^' : ACS_UARROW, l);
54 		mvwhline(win, h-1, w/2 - l/2,
55 		    conf->ascii_lines ? 'v' : ACS_DARROW, l);
56 		wattroff(win, t.dialog.arrowcolor);
57 	}
58 
59 	if (focus)
60 		wattron(win, t.menu.f_namecolor);
61 	if (strchr(fmt, 's') != NULL)
62 		mvwprintw(win, 1, 1, fmt, (const char*)value);
63 	else
64 		mvwprintw(win, 1, 1, fmt, *((const int*)value));
65 	if (focus)
66 		wattroff(win, t.menu.f_namecolor);
67 
68 	wrefresh(win);
69 }
70 
71 static int
72 datetime_autosize(struct bsddialog_conf *conf, int rows, int cols, int *h,
73     int *w, int minw, const char *text, struct buttons bs)
74 {
75 	int htext, wtext;
76 
77 	if (cols == BSDDIALOG_AUTOSIZE || rows == BSDDIALOG_AUTOSIZE) {
78 		if (text_size(conf, rows, cols, text, &bs, 3, minw, &htext,
79 		    &wtext) != 0)
80 			return (BSDDIALOG_ERROR);
81 	}
82 
83 	if (cols == BSDDIALOG_AUTOSIZE)
84 		*w = widget_min_width(conf, wtext, minw, &bs);
85 
86 	if (rows == BSDDIALOG_AUTOSIZE)
87 		*h = widget_min_height(conf, htext, 3 /* windows */, true);
88 
89 	return (0);
90 }
91 
92 static int
93 datetime_checksize(int rows, int cols, int minw, struct buttons bs)
94 {
95 	int mincols;
96 
97 	mincols = VBORDERS;
98 	mincols += buttons_min_width(bs);
99 	mincols = MAX(minw, mincols);
100 
101 	if (cols < mincols)
102 		RETURN_ERROR("Few cols for this timebox/datebox");
103 
104 	if (rows < 7) /* 2 button + 2 borders + 3 windows */
105 		RETURN_ERROR("Few rows for this timebox/datebox, at least 7");
106 
107 	return (0);
108 }
109 
110 int
111 bsddialog_timebox(struct bsddialog_conf *conf, const char* text, int rows,
112     int cols, unsigned int *hh, unsigned int *mm, unsigned int *ss)
113 {
114 	bool loop, focusbuttons;
115 	int i, retval, y, x, h, w, sel;
116 	wint_t input;
117 	WINDOW *widget, *textpad, *shadow;
118 	struct buttons bs;
119 	struct myclockstruct {
120 		unsigned int max;
121 		unsigned int value;
122 		WINDOW *win;
123 	};
124 
125 	if (hh == NULL || mm == NULL || ss == NULL)
126 		RETURN_ERROR("hh / mm / ss cannot be NULL");
127 
128 	struct myclockstruct c[3] = {
129 		{23, *hh, NULL},
130 		{59, *mm, NULL},
131 		{59, *ss, NULL}
132 	};
133 
134 	for (i = 0 ; i < 3; i++) {
135 		if (c[i].value > c[i].max)
136 			c[i].value = c[i].max;
137 	}
138 
139 	get_buttons(conf, &bs, BUTTON_OK_LABEL, BUTTON_CANCEL_LABEL);
140 
141 	if (set_widget_size(conf, rows, cols, &h, &w) != 0)
142 		return (BSDDIALOG_ERROR);
143 	if (datetime_autosize(conf, rows, cols, &h, &w, MINWTIME, text,
144 	    bs) != 0)
145 		return (BSDDIALOG_ERROR);
146 	if (datetime_checksize(h, w, MINWTIME, bs) != 0)
147 		return (BSDDIALOG_ERROR);
148 	if (set_widget_position(conf, &y, &x, h, w) != 0)
149 		return (BSDDIALOG_ERROR);
150 
151 	if (new_dialog(conf, &shadow, &widget, y, x, h, w, &textpad, text, &bs,
152 	    true) != 0)
153 		return (BSDDIALOG_ERROR);
154 
155 	pnoutrefresh(textpad, 0, 0, y+1, x+2, y+h-7, x+w-2);
156 	doupdate();
157 
158 	c[0].win = new_boxed_window(conf, y+h-6, x + w/2 - 7, 3, 4, LOWERED);
159 	mvwaddch(widget, h - 5, w/2 - 3, ':');
160 	c[1].win = new_boxed_window(conf, y+h-6, x + w/2 - 2, 3, 4, LOWERED);
161 	mvwaddch(widget, h - 5, w/2 + 2, ':');
162 	c[2].win = new_boxed_window(conf, y+h-6, x + w/2 + 3, 3, 4, LOWERED);
163 
164 	wrefresh(widget);
165 
166 	sel = -1;
167 	loop = focusbuttons = true;
168 	while (loop) {
169 		for (i = 0; i < 3; i++)
170 			drawsquare(conf, c[i].win, "%02d", &c[i].value,
171 			    sel == i);
172 
173 		if (get_wch(&input) == ERR)
174 			continue;
175 		switch(input) {
176 		case KEY_ENTER:
177 		case 10: /* Enter */
178 			if (focusbuttons || conf->button.always_active) {
179 				retval = bs.value[bs.curr];
180 				loop = false;
181 			}
182 			break;
183 		case 27: /* Esc */
184 			if (conf->key.enable_esc) {
185 				retval = BSDDIALOG_ESC;
186 				loop = false;
187 			}
188 			break;
189 		case KEY_RIGHT:
190 		case '\t': /* TAB */
191 			if (focusbuttons) {
192 				bs.curr++;
193 				focusbuttons = bs.curr < (int)bs.nbuttons ?
194 				    true : false;
195 				if (focusbuttons == false) {
196 					sel = 0;
197 					bs.curr = conf->button.always_active ? 0 : -1;
198 				}
199 			} else {
200 				sel++;
201 				focusbuttons = sel > 2 ? true : false;
202 				if (focusbuttons) {
203 					bs.curr = 0;
204 				}
205 			}
206 			draw_buttons(widget, bs, true);
207 			wrefresh(widget);
208 			break;
209 		case KEY_LEFT:
210 			if (focusbuttons) {
211 				bs.curr--;
212 				focusbuttons = bs.curr < 0 ? false : true;
213 				if (focusbuttons == false) {
214 					sel = 2;
215 					bs.curr = conf->button.always_active ? 0 : -1;
216 				}
217 			} else {
218 				sel--;
219 				focusbuttons = sel < 0 ? true : false;
220 				if (focusbuttons)
221 					bs.curr = (int)bs.nbuttons - 1;
222 			}
223 			draw_buttons(widget, bs, true);
224 			wrefresh(widget);
225 			break;
226 		case KEY_UP:
227 			if (focusbuttons) {
228 				sel = 0;
229 				focusbuttons = false;
230 				bs.curr = conf->button.always_active ? 0 : -1;
231 				draw_buttons(widget, bs, true);
232 				wrefresh(widget);
233 			} else { c[sel].value = c[sel].value > 0 ?
234 			    c[sel].value - 1 : c[sel].max;
235 			}
236 			break;
237 		case KEY_DOWN:
238 			if (focusbuttons)
239 				break;
240 			c[sel].value = c[sel].value < c[sel].max ?
241 			    c[sel].value + 1 : 0;
242 			break;
243 		case KEY_F(1):
244 			if (conf->key.f1_file == NULL &&
245 			    conf->key.f1_message == NULL)
246 				break;
247 			if (f1help(conf) != 0)
248 				return (BSDDIALOG_ERROR);
249 			/* No break, screen size can change */
250 		case KEY_RESIZE:
251 			/* Important for decreasing screen */
252 			hide_widget(y, x, h, w, conf->shadow);
253 			refresh();
254 
255 			if (set_widget_size(conf, rows, cols, &h, &w) != 0)
256 				return (BSDDIALOG_ERROR);
257 			if (datetime_autosize(conf, rows, cols, &h, &w,
258 			    MINWTIME, text, bs) != 0)
259 				return (BSDDIALOG_ERROR);
260 			if (datetime_checksize(h, w, MINWTIME, bs) != 0)
261 				return (BSDDIALOG_ERROR);
262 			if (set_widget_position(conf, &y, &x, h, w) != 0)
263 				return (BSDDIALOG_ERROR);
264 
265 			if (update_dialog(conf, shadow, widget, y, x, h, w,
266 			    textpad, text, &bs, true) != 0)
267 				return (BSDDIALOG_ERROR);
268 
269 			doupdate();
270 
271 			mvwaddch(widget, h - 5, w/2 - 3, ':');
272 			mvwaddch(widget, h - 5, w/2 + 2, ':');
273 			wrefresh(widget);
274 
275 			prefresh(textpad, 0, 0, y+1, x+2, y+h-7, x+w-2);
276 
277 			wclear(c[0].win);
278 			mvwin(c[0].win, y + h - 6, x + w/2 - 7);
279 			wclear(c[1].win);
280 			mvwin(c[1].win, y + h - 6, x + w/2 - 2);
281 			wclear(c[2].win);
282 			mvwin(c[2].win, y + h - 6, x + w/2 + 3);
283 
284 			/* Important to avoid grey lines expanding screen */
285 			refresh();
286 			break;
287 		default:
288 			if (shortcut_buttons(input, &bs)) {
289 				retval = bs.value[bs.curr];
290 				loop = false;
291 			}
292 		}
293 	}
294 
295 	if (retval == BSDDIALOG_OK) {
296 		*hh = c[0].value;
297 		*mm = c[1].value;
298 		*ss = c[2].value;
299 	}
300 
301 	for (i = 0; i < 3; i++)
302 		delwin(c[i].win);
303 	end_dialog(conf, shadow, widget, textpad);
304 
305 	return (retval);
306 }
307 
308 int
309 bsddialog_datebox(struct bsddialog_conf *conf, const char *text, int rows,
310     int cols, unsigned int *yy, unsigned int *mm, unsigned int *dd)
311 {
312 	bool loop, focusbuttons;
313 	int i, retval, y, x, h, w, sel;
314 	wint_t input;
315 	WINDOW *widget, *textpad, *shadow;
316 	struct buttons bs;
317 	struct calendar {
318 		int max;
319 		int value;
320 		WINDOW *win;
321 		unsigned int x;
322 	};
323 	struct month {
324 		const char *name;
325 		unsigned int days;
326 	};
327 
328 	if (yy == NULL || mm == NULL || dd == NULL)
329 		RETURN_ERROR("yy / mm / dd cannot be NULL");
330 
331 	struct calendar c[3] = {
332 		{9999, *yy, NULL, 4 },
333 		{12,   *mm, NULL, 9 },
334 		{31,   *dd, NULL, 2 }
335 	};
336 
337 	struct month m[12] = {
338 		{ "January", 31 }, { "February", 28 }, { "March",     31 },
339 		{ "April",   30 }, { "May",      31 }, { "June",      30 },
340 		{ "July",    31 }, { "August",   31 }, { "September", 30 },
341 		{ "October", 31 }, { "November", 30 }, { "December",  31 }
342 	};
343 
344 	for (i = 0 ; i < 3; i++) {
345 		if (c[i].value > c[i].max)
346 			c[i].value = c[i].max;
347 		if (c[i].value < 1)
348 			c[i].value = 1;
349 	}
350 	c[2].max = m[c[1].value -1].days;
351 	if (c[1].value == 2 && ISLEAP(c[0].value))
352 		c[2].max = 29;
353 	if (c[2].value > c[2].max)
354 		c[2].value = c[2].max;
355 
356 	get_buttons(conf, &bs, BUTTON_OK_LABEL, BUTTON_CANCEL_LABEL);
357 
358 	if (set_widget_size(conf, rows, cols, &h, &w) != 0)
359 		return (BSDDIALOG_ERROR);
360 	if (datetime_autosize(conf, rows, cols, &h, &w, MINWDATE, text,
361 	    bs) != 0)
362 		return (BSDDIALOG_ERROR);
363 	if (datetime_checksize(h, w, MINWDATE, bs) != 0)
364 		return (BSDDIALOG_ERROR);
365 	if (set_widget_position(conf, &y, &x, h, w) != 0)
366 		return (BSDDIALOG_ERROR);
367 
368 	if (new_dialog(conf, &shadow, &widget, y, x, h, w, &textpad, text, &bs,
369 	    true) != 0)
370 		return (BSDDIALOG_ERROR);
371 
372 	pnoutrefresh(textpad, 0, 0, y+1, x+2, y+h-7, x+w-2);
373 	doupdate();
374 
375 	c[0].win = new_boxed_window(conf, y+h-6, x + w/2 - 11, 3, 6, LOWERED);
376 	mvwaddch(widget, h - 5, w/2 - 5, '/');
377 	c[1].win = new_boxed_window(conf, y+h-6, x + w/2 - 4, 3, 11, LOWERED);
378 	mvwaddch(widget, h - 5, w/2 + 7, '/');
379 	c[2].win = new_boxed_window(conf, y+h-6, x + w/2 + 8, 3, 4, LOWERED);
380 
381 	wrefresh(widget);
382 
383 	sel = -1;
384 	loop = focusbuttons = true;
385 	while (loop) {
386 		drawsquare(conf, c[0].win, "%4d", &c[0].value, sel == 0);
387 		drawsquare(conf, c[1].win, "%9s", m[c[1].value-1].name,
388 		    sel == 1);
389 		drawsquare(conf, c[2].win, "%02d", &c[2].value, sel == 2);
390 
391 		if (get_wch(&input) == ERR)
392 			continue;
393 		switch(input) {
394 		case KEY_ENTER:
395 		case 10: /* Enter */
396 			if (focusbuttons || conf->button.always_active) {
397 				retval = bs.value[bs.curr];
398 				loop = false;
399 			}
400 			break;
401 		case 27: /* Esc */
402 			if (conf->key.enable_esc) {
403 				retval = BSDDIALOG_ESC;
404 				loop = false;
405 			}
406 			break;
407 		case KEY_RIGHT:
408 		case '\t': /* TAB */
409 			if (focusbuttons) {
410 				bs.curr++;
411 				focusbuttons = bs.curr < (int)bs.nbuttons ?
412 				    true : false;
413 				if (focusbuttons == false) {
414 					sel = 0;
415 					bs.curr = conf->button.always_active ? 0 : -1;
416 				}
417 			} else {
418 				sel++;
419 				focusbuttons = sel > 2 ? true : false;
420 				if (focusbuttons) {
421 					bs.curr = 0;
422 				}
423 			}
424 			draw_buttons(widget, bs, true);
425 			wrefresh(widget);
426 			break;
427 		case KEY_LEFT:
428 			if (focusbuttons) {
429 				bs.curr--;
430 				focusbuttons = bs.curr < 0 ? false : true;
431 				if (focusbuttons == false) {
432 					sel = 2;
433 					bs.curr = conf->button.always_active ? 0 : -1;
434 				}
435 			} else {
436 				sel--;
437 				focusbuttons = sel < 0 ? true : false;
438 				if (focusbuttons)
439 					bs.curr = (int)bs.nbuttons - 1;
440 			}
441 			draw_buttons(widget, bs, true);
442 			wrefresh(widget);
443 			break;
444 		case KEY_UP:
445 			if (focusbuttons) {
446 				sel = 0;
447 				focusbuttons = false;
448 				bs.curr = conf->button.always_active ? 0 : -1;
449 				draw_buttons(widget, bs, true);
450 				wrefresh(widget);
451 			} else {
452 				c[sel].value = c[sel].value > 1 ?
453 				    c[sel].value - 1 : c[sel].max ;
454 				/* if mount change */
455 				c[2].max = m[c[1].value -1].days;
456 				/* if year change */
457 				if (c[1].value == 2 && ISLEAP(c[0].value))
458 					c[2].max = 29;
459 				/* set new day */
460 				if (c[2].value > c[2].max)
461 					c[2].value = c[2].max;
462 			}
463 			break;
464 		case KEY_DOWN:
465 			if (focusbuttons)
466 				break;
467 			c[sel].value = c[sel].value < c[sel].max ?
468 			    c[sel].value + 1 : 1;
469 			/* if mount change */
470 			c[2].max = m[c[1].value -1].days;
471 			/* if year change */
472 			if (c[1].value == 2 && ISLEAP(c[0].value))
473 				c[2].max = 29;
474 			/* set new day */
475 			if (c[2].value > c[2].max)
476 				c[2].value = c[2].max;
477 			break;
478 		case KEY_F(1):
479 			if (conf->key.f1_file == NULL &&
480 			    conf->key.f1_message == NULL)
481 				break;
482 			if (f1help(conf) != 0)
483 				return (BSDDIALOG_ERROR);
484 			/* No break, screen size can change */
485 		case KEY_RESIZE:
486 			/* Important for decreasing screen */
487 			hide_widget(y, x, h, w, conf->shadow);
488 			refresh();
489 
490 			if (set_widget_size(conf, rows, cols, &h, &w) != 0)
491 				return (BSDDIALOG_ERROR);
492 			if (datetime_autosize(conf, rows, cols, &h, &w,
493 			    MINWDATE, text, bs) != 0)
494 				return (BSDDIALOG_ERROR);
495 			if (datetime_checksize(h, w, MINWDATE, bs) != 0)
496 				return (BSDDIALOG_ERROR);
497 			if (set_widget_position(conf, &y, &x, h, w) != 0)
498 				return (BSDDIALOG_ERROR);
499 
500 			if (update_dialog(conf, shadow, widget, y, x, h, w,
501 			    textpad, text, &bs, true) != 0)
502 				return (BSDDIALOG_ERROR);
503 			doupdate();
504 
505 			mvwaddch(widget, h - 5, w/2 - 5, '/');
506 			mvwaddch(widget, h - 5, w/2 + 7, '/');
507 			wrefresh(widget);
508 
509 			prefresh(textpad, 0, 0, y+1, x+2, y+h-7, x+w-2);
510 
511 			wclear(c[0].win);
512 			mvwin(c[0].win, y + h - 6, x + w/2 - 11);
513 			wclear(c[1].win);
514 			mvwin(c[1].win, y + h - 6, x + w/2 - 4);
515 			wclear(c[2].win);
516 			mvwin(c[2].win, y + h - 6, x + w/2 + 8);
517 
518 			/* Important to avoid grey lines expanding screen */
519 			refresh();
520 			break;
521 		default:
522 			if (shortcut_buttons(input, &bs)) {
523 				retval = bs.value[bs.curr];
524 				loop = false;
525 			}
526 		}
527 	}
528 
529 	if (retval == BSDDIALOG_OK) {
530 		*yy = c[0].value;
531 		*mm = c[1].value;
532 		*dd = c[2].value;
533 	}
534 
535 	for (i = 0; i < 3; i++)
536 		delwin(c[i].win);
537 	end_dialog(conf, shadow, widget, textpad);
538 
539 	return (retval);
540 }