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