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