xref: /freebsd/contrib/bsddialog/lib/formbox.c (revision e87ec409fa9b21abf79895837fe375ab3d7e408a)
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 <form.h>
32 #include <stdlib.h>
33 #include <string.h>
34 
35 #include "bsddialog.h"
36 #include "bsddialog_theme.h"
37 #include "lib_util.h"
38 
39 #define ISFIELDHIDDEN(item)   (item.flags & BSDDIALOG_FIELDHIDDEN)
40 #define ISFIELDREADONLY(item) (item.flags & BSDDIALOG_FIELDREADONLY)
41 #define REDRAWFORM            19860214 /* magic number */
42 
43 /* field_userptr for private buffer and view options */
44 struct myfield {
45 	int  buflen;
46 	wchar_t *buf;
47 	int  pos;
48 	int  maxpos;
49 	bool secure;
50 	int  securech;
51 	const char *bottomdesc;
52 };
53 #define GETMYFIELD(field) ((struct myfield*)field_userptr(field))
54 #define GETMYFIELD2(form) ((struct myfield*)field_userptr(current_field(form)))
55 
56 static void insertch(struct myfield *mf, int ch)
57 {
58 	int i;
59 
60 	if (mf->buflen > mf->maxpos)
61 		return;
62 
63 	for (i = mf->buflen; i >= mf->pos; i--) {
64 		mf->buf[i+1] = mf->buf[i];
65 	}
66 
67 	mf->buf[mf->pos] = ch;
68 	mf->pos += 1;
69 	if (mf->pos > mf->maxpos)
70 		mf->pos = mf->maxpos;
71 	mf->buflen += 1;
72 	mf->buf[mf->buflen] = '\0';
73 }
74 
75 static void shiftleft(struct myfield *mf)
76 {
77 	int i, last;
78 
79 	for (i = mf->pos; i < mf->buflen -1; i++) {
80 		mf->buf[i] = mf->buf[i+1];
81 	}
82 
83 	last = mf->buflen > 0 ? mf->buflen -1 : 0;
84 	mf->buf[last] = '\0';
85 		mf->buflen = last;
86 }
87 
88 static void print_bottomdesc(struct myfield *mf)
89 {
90 	move(SCREENLINES - 1, 2);
91 	clrtoeol();
92 	if (mf->bottomdesc != NULL) {
93 		addstr(mf->bottomdesc);
94 		refresh();
95 	}
96 }
97 
98 static char *w2c(wchar_t *string)
99 {
100 	int i, len;
101 	char *value;
102 
103 	len = wcslen(string);
104 	if ((value = calloc(len + 1, sizeof(char))) == NULL)
105 		return NULL;
106 
107 	for (i = 0; i < len; i++)
108 		value[i] = string[i];
109 	value[i] = '\0';
110 
111 	return value;
112 }
113 
114 static int
115 return_values(struct bsddialog_conf *conf, int output, int nitems,
116     struct bsddialog_formitem *items, FORM *form, FIELD **cfield)
117 {
118 	int i;
119 	struct myfield *mf;
120 
121 	if (output != BSDDIALOG_OK && conf->form.value_without_ok == false)
122 		return (output);
123 
124 	form_driver_w(form, KEY_CODE_YES, REQ_NEXT_FIELD);
125 	form_driver_w(form, KEY_CODE_YES, REQ_PREV_FIELD);
126 	for (i = 0; i < nitems; i++) {
127 		mf = GETMYFIELD(cfield[i]);
128 		if (conf->form.enable_wchar) {
129 			items[i].value = (char*)wcsdup(mf->buf);
130 		} else {
131 			items[i].value = w2c(mf->buf);
132 		}
133 		if (items[i].value == NULL)
134 			RETURN_ERROR("Cannot allocate memory for form value");
135 	}
136 
137 	return (output);
138 }
139 
140 static int
141 form_handler(struct bsddialog_conf *conf, WINDOW *widget, struct buttons bs,
142     WINDOW *formwin, FORM *form, FIELD **cfield, int nitems,
143     struct bsddialog_formitem *items)
144 {
145 	bool loop, buttupdate, informwin;
146 	int i, chtype, output;
147 	wint_t input;
148 	struct myfield *mf;
149 
150 	mf = GETMYFIELD2(form);
151 	print_bottomdesc(mf);
152 	pos_form_cursor(form);
153 	form_driver_w(form, KEY_CODE_YES, REQ_END_LINE);
154 	mf->pos = MIN(mf->buflen, mf->maxpos);
155 	curs_set(1);
156 	informwin = true;
157 
158 	bs.curr = -1;
159 	buttupdate = true;
160 
161 	loop = true;
162 	while (loop) {
163 		if (buttupdate) {
164 			draw_buttons(widget, bs, !informwin);
165 			wrefresh(widget);
166 			buttupdate = false;
167 		}
168 		wrefresh(formwin);
169 		chtype = get_wch(&input);
170 		if (chtype != KEY_CODE_YES && input > 127 &&
171 		    conf->form.enable_wchar == false)
172 			continue;
173 		switch(input) {
174 		case KEY_HOME:
175 		case KEY_PPAGE:
176 		case KEY_END:
177 		case KEY_NPAGE:
178 			/* disabled keys */
179 			break;
180 		case KEY_ENTER:
181 		case 10: /* Enter */
182 			if (informwin)
183 				break;
184 			output = return_values(conf, bs.value[bs.curr], nitems,
185 			    items, form, cfield);
186 			loop = false;
187 			break;
188 		case 27: /* Esc */
189 			if (conf->key.enable_esc) {
190 				output = return_values(conf, BSDDIALOG_ESC,
191 				    nitems, items, form, cfield);
192 				loop = false;
193 			}
194 			break;
195 		case '\t': /* TAB */
196 			if (informwin) {
197 				bs.curr = 0;
198 				informwin = false;
199 				curs_set(0);
200 			} else {
201 				bs.curr++;
202 				informwin = bs.curr >= (int)bs.nbuttons ?
203 				    true : false;
204 				if (informwin) {
205 					curs_set(1);
206 					pos_form_cursor(form);
207 				}
208 			}
209 			buttupdate = true;
210 			break;
211 		case KEY_LEFT:
212 			if (informwin) {
213 				form_driver_w(form, KEY_CODE_YES, REQ_PREV_CHAR);
214 				mf = GETMYFIELD2(form);
215 				if (mf->pos > 0)
216 					mf->pos -= 1;
217 			} else {
218 				if (bs.curr > 0) {
219 					bs.curr--;
220 					buttupdate = true;
221 				}
222 			}
223 			break;
224 		case KEY_RIGHT:
225 			if (informwin) {
226 				mf = GETMYFIELD2(form);
227 				if (mf->pos >= mf->buflen)
228 					break;
229 				mf->pos += 1;
230 				form_driver_w(form, KEY_CODE_YES, REQ_NEXT_CHAR);
231 			} else {
232 				if (bs.curr < (int) bs.nbuttons - 1) {
233 					bs.curr++;
234 					buttupdate = true;
235 				}
236 			}
237 			break;
238 		case KEY_UP:
239 			if (nitems < 2)
240 				break;
241 			set_field_fore(current_field(form), t.form.fieldcolor);
242 			set_field_back(current_field(form), t.form.fieldcolor);
243 			form_driver_w(form, KEY_CODE_YES, REQ_PREV_FIELD);
244 			form_driver_w(form, KEY_CODE_YES, REQ_END_LINE);
245 			mf = GETMYFIELD2(form);
246 			print_bottomdesc(mf);
247 			mf->pos = MIN(mf->buflen, mf->maxpos);
248 			set_field_fore(current_field(form), t.form.f_fieldcolor);
249 			set_field_back(current_field(form), t.form.f_fieldcolor);
250 			break;
251 		case KEY_DOWN:
252 			if (nitems < 2)
253 				break;
254 			set_field_fore(current_field(form), t.form.fieldcolor);
255 			set_field_back(current_field(form), t.form.fieldcolor);
256 			form_driver_w(form, KEY_CODE_YES, REQ_NEXT_FIELD);
257 			form_driver_w(form, KEY_CODE_YES, REQ_END_LINE);
258 			mf = GETMYFIELD2(form);
259 			print_bottomdesc(mf);
260 			mf->pos = MIN(mf->buflen, mf->maxpos);
261 			set_field_fore(current_field(form), t.form.f_fieldcolor);
262 			set_field_back(current_field(form), t.form.f_fieldcolor);
263 			break;
264 		case KEY_BACKSPACE:
265 		case 127: /* Backspace */
266 			mf = GETMYFIELD2(form);
267 			if (mf->pos <= 0)
268 				break;
269 			form_driver_w(form, KEY_CODE_YES, REQ_DEL_PREV);
270 			form_driver_w(form, KEY_CODE_YES, REQ_BEG_LINE);
271 			mf->pos = mf->pos - 1;
272 			for (i = 0; i < mf->pos; i++)
273 				form_driver_w(form, KEY_CODE_YES, REQ_NEXT_CHAR);
274 			shiftleft(mf);
275 			break;
276 		case KEY_DC:
277 			form_driver_w(form, KEY_CODE_YES, REQ_DEL_CHAR);
278 			mf = GETMYFIELD2(form);
279 			if (mf->pos < mf->buflen)
280 				shiftleft(mf);
281 			break;
282 		case KEY_F(1):
283 			if (conf->key.f1_file == NULL &&
284 			    conf->key.f1_message == NULL)
285 				break;
286 			if (f1help(conf) != 0)
287 				return (BSDDIALOG_ERROR);
288 			/* No Break */
289 		case KEY_RESIZE:
290 			output = REDRAWFORM;
291 			loop = false;
292 			break;
293 		default:
294 			if (informwin) {
295 				if (chtype == KEY_CODE_YES)
296 					break;
297 				mf = GETMYFIELD2(form);
298 				if (mf->secure)
299 					form_driver_w(form, chtype, mf->securech);
300 				else
301 					form_driver_w(form, chtype, input);
302 				insertch(mf, input);
303 			}
304 			else {
305 				if (shortcut_buttons(input, &bs)) {
306 					output = return_values(conf,
307 					    bs.value[bs.curr], nitems, items,
308 					    form, cfield);
309 					loop = false;
310 				}
311 			}
312 			break;
313 		}
314 	}
315 
316 	curs_set(0);
317 
318 	return (output);
319 }
320 
321 static int
322 form_autosize(struct bsddialog_conf *conf, int rows, int cols, int *h, int *w,
323     const char *text, int linelen, unsigned int *formheight, int nitems,
324     struct buttons bs)
325 {
326 	int htext, wtext, menusize;
327 
328 	if (cols == BSDDIALOG_AUTOSIZE || rows == BSDDIALOG_AUTOSIZE) {
329 		if (text_size(conf, rows, cols, text, &bs, *formheight + 2,
330 		    linelen + 2, &htext, &wtext) != 0)
331 			return (BSDDIALOG_ERROR);
332 	}
333 
334 	if (cols == BSDDIALOG_AUTOSIZE)
335 		*w = widget_min_width(conf, wtext, linelen + 2, &bs);
336 
337 	if (rows == BSDDIALOG_AUTOSIZE) {
338 		if (*formheight == 0) {
339 			menusize = widget_max_height(conf) - HBORDERS -
340 			     2 /*buttons*/ - htext;
341 			menusize = MIN(menusize, nitems + 2);
342 			*formheight = menusize - 2 < 0 ? 0 : menusize - 2;
343 		}
344 		else /* h autosize with fixed formheight */
345 			menusize = *formheight + 2;
346 
347 		*h = widget_min_height(conf, htext, menusize, true);
348 	} else {
349 		if (*formheight == 0)
350 			*formheight = MIN(rows-6-htext, nitems);
351 	}
352 
353 	return (0);
354 }
355 
356 static int
357 form_checksize(int rows, int cols, const char *text, int formheight, int nitems,
358     unsigned int linelen, struct buttons bs)
359 {
360 	int mincols, textrow, formrows;
361 
362 	mincols = VBORDERS;
363 	/* buttons */
364 	mincols += buttons_width(bs);
365 	mincols = MAX(mincols, (int)linelen + 4);
366 
367 	if (cols < mincols)
368 		RETURN_ERROR("Few cols, width < size buttons or "
369 		    "forms (label + field)");
370 
371 	textrow = text != NULL && strlen(text) > 0 ? 1 : 0;
372 
373 	if (nitems > 0 && formheight == 0)
374 		RETURN_ERROR("fields > 0 but formheight == 0, probably "
375 		    "terminal too small");
376 
377 	formrows = nitems > 0 ? 3 : 0;
378 	if (rows < 2  + 2 + formrows + textrow)
379 		RETURN_ERROR("Few lines for this menus");
380 
381 	return (0);
382 }
383 
384 int
385 bsddialog_form(struct bsddialog_conf *conf, const char *text, int rows,
386     int cols, unsigned int formheight, unsigned int nitems,
387     struct bsddialog_formitem *items)
388 {
389 	int i, output, color, y, x, h, w;
390 	unsigned long j, maxline, mybufsize;
391 	struct buttons bs;
392 	struct myfield *myfields;
393 	FIELD **cfield;
394 	FORM *form;
395 	WINDOW *widget, *formwin, *textpad, *shadow;
396 
397 	/* disable form scrolling */
398 	if (formheight < nitems)
399 		formheight = nitems;
400 
401 	for (i = 0; i < (int)nitems; i++) {
402 		if (items[i].maxvaluelen == 0)
403 			RETURN_ERROR("maxvaluelen cannot be zero");
404 		if (items[i].fieldlen == 0)
405 			RETURN_ERROR("fieldlen cannot be zero");
406 		if (items[i].fieldlen > items[i].maxvaluelen)
407 			RETURN_ERROR("fieldlen cannot be > maxvaluelen");
408 	}
409 
410 	maxline = 0;
411 	myfields = malloc(nitems * sizeof(struct myfield));
412 	cfield = calloc(nitems + 1, sizeof(FIELD*));
413 	for (i = 0; i < (int)nitems; i++) {
414 		cfield[i] = new_field(1, items[i].fieldlen, items[i].yfield-1,
415 		    items[i].xfield-1, 0, 0);
416 		field_opts_off(cfield[i], O_STATIC);
417 		set_max_field(cfield[i], items[i].maxvaluelen);
418 		/* setlocale() should handle set_field_buffer() */
419 		set_field_buffer(cfield[i], 0, items[i].init);
420 
421 		mybufsize = (items[i].maxvaluelen + 1) * sizeof(wchar_t);
422 		myfields[i].buf = malloc(mybufsize);
423 		memset(myfields[i].buf, 0, mybufsize);
424 		for (j = 0; j < items[i].maxvaluelen && j < strlen(items[i].init);
425 		    j++)
426 			myfields[i].buf[j] = items[i].init[j];
427 
428 		myfields[i].buflen = wcslen(myfields[i].buf);
429 
430 		myfields[i].maxpos = items[i].maxvaluelen -1;
431 		myfields[i].pos = MIN(myfields[i].buflen, myfields[i].maxpos);
432 
433 		myfields[i].bottomdesc = items[i].bottomdesc;
434 		set_field_userptr(cfield[i], &myfields[i]);
435 
436 		field_opts_off(cfield[i], O_AUTOSKIP);
437 		field_opts_off(cfield[i], O_BLANK);
438 
439 		if (ISFIELDHIDDEN(items[i])) {
440 			myfields[i].secure = true;
441 			myfields[i].securech = ' ';
442 			if (conf->form.securech != '\0')
443 				myfields[i].securech = conf->form.securech;
444 		}
445 		else
446 			myfields[i].secure = false;
447 
448 		if (ISFIELDREADONLY(items[i])) {
449 			field_opts_off(cfield[i], O_EDIT);
450 			field_opts_off(cfield[i], O_ACTIVE);
451 			color = t.form.readonlycolor;
452 		} else {
453 			color = i == 0 ? t.form.f_fieldcolor : t.form.fieldcolor;
454 		}
455 		set_field_fore(cfield[i], color);
456 		set_field_back(cfield[i], color);
457 
458 		maxline = MAX(maxline, items[i].xlabel + strlen(items[i].label));
459 		maxline = MAX(maxline, items[i].xfield + items[i].fieldlen - 1);
460 	}
461 	cfield[i] = NULL;
462 
463 	 /* disable focus with 1 item (inputbox or passwordbox) */
464 	if (formheight == 1 && nitems == 1 && strlen(items[0].label) == 0 &&
465 	    items[0].xfield == 1 ) {
466 		set_field_fore(cfield[0], t.dialog.color);
467 		set_field_back(cfield[0], t.dialog.color);
468 	}
469 
470 	get_buttons(conf, &bs, BUTTON_OK_LABEL, BUTTON_CANCEL_LABEL);
471 
472 	if (set_widget_size(conf, rows, cols, &h, &w) != 0)
473 		return (BSDDIALOG_ERROR);
474 	if (form_autosize(conf, rows, cols, &h, &w, text, maxline, &formheight,
475 	    nitems, bs) != 0)
476 		return (BSDDIALOG_ERROR);
477 	if (form_checksize(h, w, text, formheight, nitems, maxline, bs) != 0)
478 		return (BSDDIALOG_ERROR);
479 	if (set_widget_position(conf, &y, &x, h, w) != 0)
480 		return (BSDDIALOG_ERROR);
481 
482 	if (new_dialog(conf, &shadow, &widget, y, x, h, w, &textpad, text, &bs,
483 	    true) != 0)
484 		return (BSDDIALOG_ERROR);
485 
486 	prefresh(textpad, 0, 0, y + 1, x + 1 + TEXTHMARGIN,
487 	    y + h - formheight, x + 1 + w - TEXTHMARGIN);
488 
489 	formwin = new_boxed_window(conf, y + h - 3 - formheight -2, x +1,
490 	    formheight+2, w-2, LOWERED);
491 
492 	form = new_form(cfield);
493 	set_form_win(form, formwin);
494 	/* should be formheight */
495 	set_form_sub(form, derwin(formwin, nitems, w-4, 1, 1));
496 	post_form(form);
497 
498 	for (i = 0; i < (int)nitems; i++)
499 		mvwaddstr(formwin, items[i].ylabel, items[i].xlabel,
500 		    items[i].label);
501 
502 	wrefresh(formwin);
503 
504 	do {
505 		output = form_handler(conf, widget, bs, formwin, form, cfield,
506 		    nitems, items);
507 
508 		if (update_dialog(conf, shadow, widget, y, x, h, w, textpad,
509 		    text, &bs, true) != 0)
510 			return (BSDDIALOG_ERROR);
511 
512 		doupdate();
513 		wrefresh(widget);
514 
515 		prefresh(textpad, 0, 0, y + 1, x + 1 + TEXTHMARGIN,
516 		    y + h - formheight, x + 1 + w - TEXTHMARGIN);
517 
518 		draw_borders(conf, formwin, formheight+2, w-2, LOWERED);
519 		wrefresh(formwin);
520 
521 		refresh();
522 	} while (output == REDRAWFORM);
523 
524 	unpost_form(form);
525 	free_form(form);
526 	for (i = 0; i < (int)nitems; i++) {
527 		free_field(cfield[i]);
528 		free(myfields[i].buf);
529 	}
530 	free(cfield);
531 	free(myfields);
532 
533 	delwin(formwin);
534 	end_dialog(conf, shadow, widget, textpad);
535 
536 	return (output);
537 }
538