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