xref: /freebsd/contrib/bsddialog/lib/timebox.c (revision b197d4b893974c9eb4d7b38704c6d5c486235d6f)
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 drawquare(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 			drawquare(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 #define ISLEAF(year) ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0)
345 
346 	for (i = 0 ; i < 3; i++) {
347 		if (c[i].value > c[i].max)
348 			c[i].value = c[i].max;
349 		if (c[i].value < 1)
350 			c[i].value = 1;
351 	}
352 	c[2].max = m[c[1].value -1].days;
353 	if (c[1].value == 2 && ISLEAF(c[0].value))
354 		c[2].max = 29;
355 	if (c[2].value > c[2].max)
356 		c[2].value = c[2].max;
357 
358 	get_buttons(conf, &bs, BUTTON_OK_LABEL, BUTTON_CANCEL_LABEL);
359 
360 	if (set_widget_size(conf, rows, cols, &h, &w) != 0)
361 		return (BSDDIALOG_ERROR);
362 	if (datetime_autosize(conf, rows, cols, &h, &w, MINWDATE, text,
363 	    bs) != 0)
364 		return (BSDDIALOG_ERROR);
365 	if (datetime_checksize(h, w, MINWDATE, bs) != 0)
366 		return (BSDDIALOG_ERROR);
367 	if (set_widget_position(conf, &y, &x, h, w) != 0)
368 		return (BSDDIALOG_ERROR);
369 
370 	if (new_dialog(conf, &shadow, &widget, y, x, h, w, &textpad, text, &bs,
371 	    true) != 0)
372 		return (BSDDIALOG_ERROR);
373 
374 	pnoutrefresh(textpad, 0, 0, y+1, x+2, y+h-7, x+w-2);
375 	doupdate();
376 
377 	c[0].win = new_boxed_window(conf, y+h-6, x + w/2 - 11, 3, 6, LOWERED);
378 	mvwaddch(widget, h - 5, w/2 - 5, '/');
379 	c[1].win = new_boxed_window(conf, y+h-6, x + w/2 - 4, 3, 11, LOWERED);
380 	mvwaddch(widget, h - 5, w/2 + 7, '/');
381 	c[2].win = new_boxed_window(conf, y+h-6, x + w/2 + 8, 3, 4, LOWERED);
382 
383 	wrefresh(widget);
384 
385 	sel = -1;
386 	loop = focusbuttons = true;
387 	while (loop) {
388 		drawquare(conf, c[0].win, "%4d", &c[0].value, sel == 0);
389 		drawquare(conf, c[1].win, "%9s", m[c[1].value-1].name,
390 		    sel == 1);
391 		drawquare(conf, c[2].win, "%02d", &c[2].value, sel == 2);
392 
393 		if (get_wch(&input) == ERR)
394 			continue;
395 		switch(input) {
396 		case KEY_ENTER:
397 		case 10: /* Enter */
398 			if (focusbuttons || conf->button.always_active) {
399 				retval = bs.value[bs.curr];
400 				loop = false;
401 			}
402 			break;
403 		case 27: /* Esc */
404 			if (conf->key.enable_esc) {
405 				retval = BSDDIALOG_ESC;
406 				loop = false;
407 			}
408 			break;
409 		case KEY_RIGHT:
410 		case '\t': /* TAB */
411 			if (focusbuttons) {
412 				bs.curr++;
413 				focusbuttons = bs.curr < (int)bs.nbuttons ?
414 				    true : false;
415 				if (focusbuttons == false) {
416 					sel = 0;
417 					bs.curr = conf->button.always_active ? 0 : -1;
418 				}
419 			} else {
420 				sel++;
421 				focusbuttons = sel > 2 ? true : false;
422 				if (focusbuttons) {
423 					bs.curr = 0;
424 				}
425 			}
426 			draw_buttons(widget, bs, true);
427 			wrefresh(widget);
428 			break;
429 		case KEY_LEFT:
430 			if (focusbuttons) {
431 				bs.curr--;
432 				focusbuttons = bs.curr < 0 ? false : true;
433 				if (focusbuttons == false) {
434 					sel = 2;
435 					bs.curr = conf->button.always_active ? 0 : -1;
436 				}
437 			} else {
438 				sel--;
439 				focusbuttons = sel < 0 ? true : false;
440 				if (focusbuttons)
441 					bs.curr = (int)bs.nbuttons - 1;
442 			}
443 			draw_buttons(widget, bs, true);
444 			wrefresh(widget);
445 			break;
446 		case KEY_UP:
447 			if (focusbuttons) {
448 				sel = 0;
449 				focusbuttons = false;
450 				bs.curr = conf->button.always_active ? 0 : -1;
451 				draw_buttons(widget, bs, true);
452 				wrefresh(widget);
453 			} else {
454 				c[sel].value = c[sel].value > 1 ?
455 				    c[sel].value - 1 : c[sel].max ;
456 				/* if mount change */
457 				c[2].max = m[c[1].value -1].days;
458 				/* if year change */
459 				if (c[1].value == 2 && ISLEAF(c[0].value))
460 					c[2].max = 29;
461 				/* set new day */
462 				if (c[2].value > c[2].max)
463 					c[2].value = c[2].max;
464 			}
465 			break;
466 		case KEY_DOWN:
467 			if (focusbuttons)
468 				break;
469 			c[sel].value = c[sel].value < c[sel].max ?
470 			    c[sel].value + 1 : 1;
471 			/* if mount change */
472 			c[2].max = m[c[1].value -1].days;
473 			/* if year change */
474 			if (c[1].value == 2 && ISLEAF(c[0].value))
475 				c[2].max = 29;
476 			/* set new day */
477 			if (c[2].value > c[2].max)
478 				c[2].value = c[2].max;
479 			break;
480 		case KEY_F(1):
481 			if (conf->key.f1_file == NULL &&
482 			    conf->key.f1_message == NULL)
483 				break;
484 			if (f1help(conf) != 0)
485 				return (BSDDIALOG_ERROR);
486 			/* No break, screen size can change */
487 		case KEY_RESIZE:
488 			/* Important for decreasing screen */
489 			hide_widget(y, x, h, w, conf->shadow);
490 			refresh();
491 
492 			if (set_widget_size(conf, rows, cols, &h, &w) != 0)
493 				return (BSDDIALOG_ERROR);
494 			if (datetime_autosize(conf, rows, cols, &h, &w,
495 			    MINWDATE, text, bs) != 0)
496 				return (BSDDIALOG_ERROR);
497 			if (datetime_checksize(h, w, MINWDATE, bs) != 0)
498 				return (BSDDIALOG_ERROR);
499 			if (set_widget_position(conf, &y, &x, h, w) != 0)
500 				return (BSDDIALOG_ERROR);
501 
502 			if (update_dialog(conf, shadow, widget, y, x, h, w,
503 			    textpad, text, &bs, true) != 0)
504 				return (BSDDIALOG_ERROR);
505 			doupdate();
506 
507 			mvwaddch(widget, h - 5, w/2 - 5, '/');
508 			mvwaddch(widget, h - 5, w/2 + 7, '/');
509 			wrefresh(widget);
510 
511 			prefresh(textpad, 0, 0, y+1, x+2, y+h-7, x+w-2);
512 
513 			wclear(c[0].win);
514 			mvwin(c[0].win, y + h - 6, x + w/2 - 11);
515 			wclear(c[1].win);
516 			mvwin(c[1].win, y + h - 6, x + w/2 - 4);
517 			wclear(c[2].win);
518 			mvwin(c[2].win, y + h - 6, x + w/2 + 8);
519 
520 			/* Important to avoid grey lines expanding screen */
521 			refresh();
522 			break;
523 		default:
524 			if (shortcut_buttons(input, &bs)) {
525 				retval = bs.value[bs.curr];
526 				loop = false;
527 			}
528 		}
529 	}
530 
531 	if (retval == BSDDIALOG_OK) {
532 		*yy = c[0].value;
533 		*mm = c[1].value;
534 		*dd = c[2].value;
535 	}
536 
537 	for (i = 0; i < 3; i++)
538 		delwin(c[i].win);
539 	end_dialog(conf, shadow, widget, textpad);
540 
541 	return (retval);
542 }
543