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