1/*
2 *  $Id: inputstr.c,v 1.91 2021/01/17 22:19:05 tom Exp $
3 *
4 *  inputstr.c -- functions for input/display of a string
5 *
6 *  Copyright 2000-2019,2021	Thomas E. Dickey
7 *
8 *  This program is free software; you can redistribute it and/or modify
9 *  it under the terms of the GNU Lesser General Public License, version 2.1
10 *  as published by the Free Software Foundation.
11 *
12 *  This program is distributed in the hope that it will be useful, but
13 *  WITHOUT ANY WARRANTY; without even the implied warranty of
14 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15 *  Lesser General Public License for more details.
16 *
17 *  You should have received a copy of the GNU Lesser General Public
18 *  License along with this program; if not, write to
19 *	Free Software Foundation, Inc.
20 *	51 Franklin St., Fifth Floor
21 *	Boston, MA 02110, USA.
22 */
23
24#include <dialog.h>
25#include <dlg_keys.h>
26
27#include <errno.h>
28
29#ifdef HAVE_SETLOCALE
30#include <locale.h>
31#endif
32
33#if defined(HAVE_SEARCH_H) && defined(HAVE_TSEARCH)
34#include <search.h>
35#else
36#undef HAVE_TSEARCH
37#endif
38
39#ifdef NEED_WCHAR_H
40#include <wchar.h>
41#endif
42
43#if defined(USE_WIDE_CURSES)
44#define USE_CACHING 1
45#elif defined(HAVE_XDIALOG)
46#define USE_CACHING 1		/* editbox really needs caching! */
47#else
48#define USE_CACHING 0
49#endif
50
51typedef struct _cache {
52    struct _cache *next;
53#if USE_CACHING
54    int cache_num;		/* tells what type of data is in list[] */
55    const char *string_at;	/* unique: associate caches by char* */
56#endif
57    size_t s_len;		/* strlen(string) - we add 1 for EOS */
58    size_t i_len;		/* length(list) - we add 1 for EOS */
59    char *string;		/* a copy of the last-processed string */
60    int *list;			/* indices into the string */
61} CACHE;
62
63#if USE_CACHING
64#define SAME_CACHE(c,s,l) (c->string != 0 && memcmp(c->string,s,l) == 0)
65
66static CACHE *cache_list;
67
68typedef enum {
69    cInxCols
70    ,cCntWideBytes
71    ,cCntWideChars
72    ,cInxWideChars
73    ,cMAX
74} CACHE_USED;
75
76#ifdef HAVE_TSEARCH
77static void *sorted_cache;
78#endif
79
80#ifdef USE_WIDE_CURSES
81static int
82have_locale(void)
83{
84    static int result = -1;
85    if (result < 0) {
86	char *test = setlocale(LC_ALL, 0);
87	if (test == 0 || *test == 0) {
88	    result = FALSE;
89	} else if (strcmp(test, "C") && strcmp(test, "POSIX")) {
90	    result = TRUE;
91	} else {
92	    result = FALSE;
93	}
94    }
95    return result;
96}
97#endif
98
99#ifdef HAVE_TSEARCH
100
101#if 0
102static void
103show_tsearch(const void *nodep, const VISIT which, const int depth)
104{
105    const CACHE *p = *(CACHE * const *) nodep;
106    (void) depth;
107    if (which == postorder || which == leaf) {
108	DLG_TRACE(("# cache %p %p:%s\n", p, p->string, p->string));
109    }
110}
111
112static void
113trace_cache(const char *fn, int ln)
114{
115    DLG_TRACE(("# trace_cache %s@%d\n", fn, ln));
116    twalk(sorted_cache, show_tsearch);
117}
118
119#else
120#define trace_cache(fn, ln)	/* nothing */
121#endif
122
123#define CMP(a,b) (((a) > (b)) ? 1 : (((a) < (b)) ? -1 : 0))
124
125static int
126compare_cache(const void *a, const void *b)
127{
128    const CACHE *p = (const CACHE *) a;
129    const CACHE *q = (const CACHE *) b;
130    int result = CMP(p->cache_num, q->cache_num);
131    if (result == 0)
132	result = CMP(p->string_at, q->string_at);
133    return result;
134}
135#endif
136
137static CACHE *
138find_cache(int cache_num, const char *string)
139{
140    CACHE *p;
141
142#ifdef HAVE_TSEARCH
143    void *pp;
144    CACHE find;
145
146    memset(&find, 0, sizeof(find));
147    find.cache_num = cache_num;
148    find.string_at = string;
149
150    if ((pp = tfind(&find, &sorted_cache, compare_cache)) != 0) {
151	p = *(CACHE **) pp;
152    } else {
153	p = 0;
154    }
155#else
156    for (p = cache_list; p != 0; p = p->next) {
157	if (p->string_at == string) {
158	    break;
159	}
160    }
161#endif
162    return p;
163}
164
165static CACHE *
166make_cache(int cache_num, const char *string)
167{
168    CACHE *p;
169
170    p = dlg_calloc(CACHE, 1);
171    assert_ptr(p, "load_cache");
172    p->next = cache_list;
173    cache_list = p;
174
175    p->cache_num = cache_num;
176    p->string_at = string;
177
178#ifdef HAVE_TSEARCH
179    (void) tsearch(p, &sorted_cache, compare_cache);
180#endif
181    return p;
182}
183
184static CACHE *
185load_cache(int cache_num, const char *string)
186{
187    CACHE *p;
188
189    if ((p = find_cache(cache_num, string)) == 0) {
190	p = make_cache(cache_num, string);
191    }
192    return p;
193}
194#else
195static CACHE my_cache;
196#define SAME_CACHE(c,s,l) (c->string != 0)
197#define load_cache(cache, string) &my_cache
198#endif /* USE_CACHING */
199
200/*
201 * If the given string has not changed, we do not need to update the index.
202 * If we need to update the index, allocate enough memory for it.
203 */
204static bool
205same_cache2(CACHE * cache, const char *string, unsigned i_len)
206{
207    size_t s_len = strlen(string);
208    bool result = TRUE;
209
210    if (cache->s_len == 0
211	|| cache->s_len < s_len
212	|| cache->list == 0
213	|| !SAME_CACHE(cache, string, (size_t) s_len)) {
214	unsigned need = (i_len + 1);
215
216	if (cache->list == 0) {
217	    cache->list = dlg_malloc(int, need);
218	} else if (cache->i_len < i_len) {
219	    cache->list = dlg_realloc(int, need, cache->list);
220	}
221	assert_ptr(cache->list, "load_cache");
222	cache->i_len = i_len;
223
224	if (cache->s_len >= s_len && cache->string != 0) {
225	    strcpy(cache->string, string);
226	} else {
227	    if (cache->string != 0)
228		free(cache->string);
229	    cache->string = dlg_strclone(string);
230	}
231	cache->s_len = s_len;
232
233	result = FALSE;
234    }
235    return result;
236}
237
238#ifdef USE_WIDE_CURSES
239/*
240 * Like same_cache2(), but we are only concerned about caching a copy of the
241 * string and its associated length.
242 */
243static bool
244same_cache1(CACHE * cache, const char *string, size_t i_len)
245{
246    size_t s_len = strlen(string);
247    bool result = TRUE;
248
249    if (cache->s_len != s_len
250	|| !SAME_CACHE(cache, string, (size_t) s_len)) {
251
252	if (cache->s_len >= s_len && cache->string != 0) {
253	    strcpy(cache->string, string);
254	} else {
255	    if (cache->string != 0)
256		free(cache->string);
257	    cache->string = dlg_strclone(string);
258	}
259	cache->s_len = s_len;
260	cache->i_len = i_len;
261
262	result = FALSE;
263    }
264    return result;
265}
266#endif /* USE_CACHING */
267
268/*
269 * Counts the number of bytes that make up complete wide-characters, up to byte
270 * 'len'.  If there is no locale set, simply return the original length.
271 */
272#ifdef USE_WIDE_CURSES
273static int
274dlg_count_wcbytes(const char *string, size_t len)
275{
276    int result;
277
278    if (have_locale()) {
279	CACHE *cache = load_cache(cCntWideBytes, string);
280	if (!same_cache1(cache, string, len)) {
281	    while (len != 0) {
282		size_t code = 0;
283		const char *src = cache->string;
284		mbstate_t state;
285		char save = cache->string[len];
286
287		cache->string[len] = '\0';
288		memset(&state, 0, sizeof(state));
289		code = mbsrtowcs((wchar_t *) 0, &src, len, &state);
290		cache->string[len] = save;
291		if ((int) code >= 0) {
292		    break;
293		}
294		--len;
295	    }
296	    cache->i_len = len;
297	}
298	result = (int) cache->i_len;
299    } else {
300	result = (int) len;
301    }
302    return result;
303}
304#endif /* USE_WIDE_CURSES */
305
306/*
307 * Counts the number of wide-characters in the string.
308 */
309int
310dlg_count_wchars(const char *string)
311{
312    int result;
313#ifdef USE_WIDE_CURSES
314
315    if (have_locale()) {
316	size_t len = strlen(string);
317	CACHE *cache = load_cache(cCntWideChars, string);
318
319	if (!same_cache1(cache, string, len)) {
320	    const char *src = cache->string;
321	    mbstate_t state;
322	    int part = dlg_count_wcbytes(cache->string, len);
323	    char save = cache->string[part];
324	    wchar_t *temp = dlg_calloc(wchar_t, len + 1);
325
326	    if (temp != 0) {
327		size_t code;
328
329		cache->string[part] = '\0';
330		memset(&state, 0, sizeof(state));
331		code = mbsrtowcs(temp, &src, (size_t) part, &state);
332		cache->i_len = ((int) code >= 0) ? wcslen(temp) : 0;
333		cache->string[part] = save;
334		free(temp);
335	    } else {
336		cache->i_len = 0;
337	    }
338	}
339	result = (int) cache->i_len;
340    } else
341#endif /* USE_WIDE_CURSES */
342    {
343	result = (int) strlen(string);
344    }
345    return result;
346}
347
348/*
349 * Build an index of the wide-characters in the string, so we can easily tell
350 * which byte-offset begins a given wide-character.
351 */
352const int *
353dlg_index_wchars(const char *string)
354{
355    unsigned len = (unsigned) dlg_count_wchars(string);
356    CACHE *cache = load_cache(cInxWideChars, string);
357
358    if (!same_cache2(cache, string, len)) {
359	const char *current = string;
360	unsigned inx;
361
362	cache->list[0] = 0;
363	for (inx = 1; inx <= len; ++inx) {
364#ifdef USE_WIDE_CURSES
365	    if (have_locale()) {
366		mbstate_t state;
367		int width;
368		memset(&state, 0, sizeof(state));
369		width = (int) mbrlen(current, strlen(current), &state);
370		if (width <= 0)
371		    width = 1;	/* FIXME: what if we have a control-char? */
372		current += width;
373		cache->list[inx] = cache->list[inx - 1] + width;
374	    } else
375#endif /* USE_WIDE_CURSES */
376	    {
377		(void) current;
378		cache->list[inx] = (int) inx;
379	    }
380	}
381    }
382    return cache->list;
383}
384
385/*
386 * Given the character-offset to find in the list, return the corresponding
387 * array index.
388 */
389int
390dlg_find_index(const int *list, int limit, int to_find)
391{
392    int result;
393    for (result = 0; result <= limit; ++result) {
394	if (to_find == list[result]
395	    || result == limit
396	    || ((result < limit) && (to_find < list[result + 1]))) {
397	    break;
398	}
399    }
400    return result;
401}
402
403/*
404 * Build a list of the display-columns for the given string's characters.
405 */
406const int *
407dlg_index_columns(const char *string)
408{
409    unsigned len = (unsigned) dlg_count_wchars(string);
410    CACHE *cache = load_cache(cInxCols, string);
411
412    if (!same_cache2(cache, string, len)) {
413
414	cache->list[0] = 0;
415#ifdef USE_WIDE_CURSES
416	if (have_locale()) {
417	    unsigned inx;
418	    size_t num_bytes = strlen(string);
419	    const int *inx_wchars = dlg_index_wchars(string);
420	    mbstate_t state;
421
422	    for (inx = 0; inx < len; ++inx) {
423		int result;
424
425		if (string[inx_wchars[inx]] == TAB) {
426		    result = ((cache->list[inx] | 7) + 1) - cache->list[inx];
427		} else {
428		    wchar_t temp[2];
429		    size_t check;
430
431		    memset(&state, 0, sizeof(state));
432		    memset(temp, 0, sizeof(temp));
433		    check = mbrtowc(temp,
434				    string + inx_wchars[inx],
435				    num_bytes - (size_t) inx_wchars[inx],
436				    &state);
437		    if ((int) check <= 0) {
438			result = 1;
439		    } else {
440			result = wcwidth(temp[0]);
441		    }
442		    if (result < 0) {
443			const wchar_t *printable;
444			cchar_t temp2, *temp2p = &temp2;
445			setcchar(temp2p, temp, 0, 0, 0);
446			printable = wunctrl(temp2p);
447			result = printable ? (int) wcslen(printable) : 1;
448		    }
449		}
450		cache->list[inx + 1] = result;
451		if (inx != 0)
452		    cache->list[inx + 1] += cache->list[inx];
453	    }
454	} else
455#endif /* USE_WIDE_CURSES */
456	{
457	    unsigned inx;
458
459	    for (inx = 0; inx < len; ++inx) {
460		chtype ch = UCH(string[inx]);
461
462		if (ch == TAB)
463		    cache->list[inx + 1] =
464			((cache->list[inx] | 7) + 1) - cache->list[inx];
465		else if (isprint(UCH(ch)))
466		    cache->list[inx + 1] = 1;
467		else {
468		    const char *printable;
469		    printable = unctrl(ch);
470		    cache->list[inx + 1] = (printable
471					    ? (int) strlen(printable)
472					    : 1);
473		}
474		if (inx != 0)
475		    cache->list[inx + 1] += cache->list[inx];
476	    }
477	}
478    }
479    return cache->list;
480}
481
482/*
483 * Returns the number of columns used for a string.  That happens to be the
484 * end-value of the cols[] array.
485 */
486int
487dlg_count_columns(const char *string)
488{
489    int result = 0;
490    int limit = dlg_count_wchars(string);
491    if (limit > 0) {
492	const int *cols = dlg_index_columns(string);
493	result = cols[limit];
494    } else {
495	result = (int) strlen(string);
496    }
497    dlg_finish_string(string);
498    return result;
499}
500
501/*
502 * Given a column limit, count the number of wide characters that can fit
503 * into that limit.  The offset is used to skip over a leading character
504 * that was already written.
505 */
506int
507dlg_limit_columns(const char *string, int limit, int offset)
508{
509    const int *cols = dlg_index_columns(string);
510    int result = dlg_count_wchars(string);
511
512    while (result > 0 && (cols[result] - cols[offset]) > limit)
513	--result;
514    return result;
515}
516
517/*
518 * Updates the string and character-offset, given various editing characters
519 * or literal characters which are inserted at the character-offset.
520 */
521bool
522dlg_edit_string(char *string, int *chr_offset, int key, int fkey, bool force)
523{
524    int i;
525    int len = (int) strlen(string);
526    int limit = dlg_count_wchars(string);
527    const int *indx = dlg_index_wchars(string);
528    int offset = dlg_find_index(indx, limit, *chr_offset);
529    int max_len = dlg_max_input(MAX_LEN);
530    bool edit = TRUE;
531
532    /* transform editing characters into equivalent function-keys */
533    if (!fkey) {
534	fkey = TRUE;		/* assume we transform */
535	switch (key) {
536	case 0:
537	    break;
538	case ESC:
539	case TAB:
540	    fkey = FALSE;	/* this is used for navigation */
541	    break;
542	default:
543	    fkey = FALSE;	/* ...no, we did not transform */
544	    break;
545	}
546    }
547
548    if (fkey) {
549	switch (key) {
550	case 0:		/* special case for loop entry */
551	    edit = force;
552	    break;
553	case DLGK_GRID_LEFT:
554	    if (*chr_offset && offset > 0)
555		*chr_offset = indx[offset - 1];
556	    break;
557	case DLGK_GRID_RIGHT:
558	    if (offset < limit)
559		*chr_offset = indx[offset + 1];
560	    break;
561	case DLGK_BEGIN:
562	    if (*chr_offset)
563		*chr_offset = 0;
564	    break;
565	case DLGK_FINAL:
566	    if (offset < limit)
567		*chr_offset = indx[limit];
568	    break;
569	case DLGK_DELETE_LEFT:
570	    if (offset) {
571		int gap = indx[offset] - indx[offset - 1];
572		*chr_offset = indx[offset - 1];
573		if (gap > 0) {
574		    for (i = *chr_offset;
575			 (string[i] = string[i + gap]) != '\0';
576			 i++) {
577			;
578		    }
579		}
580	    }
581	    break;
582	case DLGK_DELETE_RIGHT:
583	    if (limit) {
584		if (--limit == 0) {
585		    string[*chr_offset = 0] = '\0';
586		} else {
587		    int gap = ((offset <= limit)
588			       ? (indx[offset + 1] - indx[offset])
589			       : 0);
590		    if (gap > 0) {
591			for (i = indx[offset];
592			     (string[i] = string[i + gap]) != '\0';
593			     i++) {
594			    ;
595			}
596		    } else if (offset > 0) {
597			string[indx[offset - 1]] = '\0';
598		    }
599		    if (*chr_offset > indx[limit])
600			*chr_offset = indx[limit];
601		}
602	    }
603	    break;
604	case DLGK_DELETE_ALL:
605	    string[*chr_offset = 0] = '\0';
606	    break;
607	case DLGK_ENTER:
608	    edit = 0;
609	    break;
610#ifdef KEY_RESIZE
611	case KEY_RESIZE:
612	    edit = 0;
613	    break;
614#endif
615	case DLGK_GRID_UP:
616	case DLGK_GRID_DOWN:
617	case DLGK_FIELD_NEXT:
618	case DLGK_FIELD_PREV:
619	    edit = 0;
620	    break;
621	case ERR:
622	    edit = 0;
623	    break;
624	default:
625	    beep();
626	    break;
627	}
628    } else {
629	if (key == ESC || key == ERR) {
630	    edit = 0;
631	} else {
632	    if (len < max_len) {
633		for (i = ++len; i > *chr_offset; i--)
634		    string[i] = string[i - 1];
635		string[*chr_offset] = (char) key;
636		*chr_offset += 1;
637	    } else {
638		(void) beep();
639	    }
640	}
641    }
642    return edit;
643}
644
645static void
646compute_edit_offset(const char *string,
647		    int chr_offset,
648		    int x_last,
649		    int *p_dpy_column,
650		    int *p_scroll_amt)
651{
652    const int *cols = dlg_index_columns(string);
653    const int *indx = dlg_index_wchars(string);
654    int limit = dlg_count_wchars(string);
655    int offset = dlg_find_index(indx, limit, chr_offset);
656    int offset2;
657    int dpy_column;
658    int n;
659
660    for (n = offset2 = 0; n <= offset; ++n) {
661	if ((cols[offset] - cols[n]) < x_last
662	    && (offset == limit || (cols[offset + 1] - cols[n]) < x_last)) {
663	    offset2 = n;
664	    break;
665	}
666    }
667
668    dpy_column = cols[offset] - cols[offset2];
669
670    if (p_dpy_column != 0)
671	*p_dpy_column = dpy_column;
672    if (p_scroll_amt != 0)
673	*p_scroll_amt = offset2;
674}
675
676/*
677 * Given the character-offset in the string, returns the display-offset where
678 * we will position the cursor.
679 */
680int
681dlg_edit_offset(char *string, int chr_offset, int x_last)
682{
683    int result;
684
685    compute_edit_offset(string, chr_offset, x_last, &result, 0);
686
687    return result;
688}
689
690/*
691 * Displays the string, shifted as necessary, to fit within the box and show
692 * the current character-offset.
693 */
694void
695dlg_show_string(WINDOW *win,
696		const char *string,	/* string to display (may be multibyte) */
697		int chr_offset,	/* character (not bytes) offset */
698		chtype attr,	/* window-attributes */
699		int y_base,	/* beginning row on screen */
700		int x_base,	/* beginning column on screen */
701		int x_last,	/* number of columns on screen */
702		bool hidden,	/* if true, do not echo */
703		bool force)	/* if true, force repaint */
704{
705    x_last = MIN(x_last + x_base, getmaxx(win)) - x_base;
706
707    if (hidden && !dialog_vars.insecure) {
708	if (force) {
709	    (void) wmove(win, y_base, x_base);
710	    wrefresh(win);
711	}
712    } else {
713	const int *cols = dlg_index_columns(string);
714	const int *indx = dlg_index_wchars(string);
715	int limit = dlg_count_wchars(string);
716
717	int i, j, k;
718	int input_x;
719	int scrollamt;
720
721	compute_edit_offset(string, chr_offset, x_last, &input_x, &scrollamt);
722
723	dlg_attrset(win, attr);
724	(void) wmove(win, y_base, x_base);
725	for (i = scrollamt, k = 0; i < limit && k < x_last; ++i) {
726	    int check = cols[i + 1] - cols[scrollamt];
727	    if (check <= x_last) {
728		for (j = indx[i]; j < indx[i + 1]; ++j) {
729		    chtype ch = UCH(string[j]);
730		    if (hidden && dialog_vars.insecure) {
731			waddch(win, '*');
732		    } else if (ch == TAB) {
733			int count = cols[i + 1] - cols[i];
734			while (--count >= 0)
735			    waddch(win, ' ');
736		    } else {
737			waddch(win, ch);
738		    }
739		}
740		k = check;
741	    } else {
742		break;
743	    }
744	}
745	while (k++ < x_last)
746	    waddch(win, ' ');
747	(void) wmove(win, y_base, x_base + input_x);
748	wrefresh(win);
749    }
750}
751
752/*
753 * Discard cached data for the given string.
754 */
755void
756dlg_finish_string(const char *string)
757{
758#if USE_CACHING
759    if ((string != 0) && dialog_state.finish_string) {
760	CACHE *p = cache_list;
761	CACHE *q = 0;
762	CACHE *r;
763
764	while (p != 0) {
765	    if (p->string_at == string) {
766#ifdef HAVE_TSEARCH
767		if (tdelete(p, &sorted_cache, compare_cache) == 0) {
768		    continue;
769		}
770		trace_cache(__FILE__, __LINE__);
771#endif
772		if (p->string != 0)
773		    free(p->string);
774		if (p->list != 0)
775		    free(p->list);
776		if (p == cache_list) {
777		    cache_list = p->next;
778		    r = cache_list;
779		} else {
780		    q->next = p->next;
781		    r = q;
782		}
783		free(p);
784		p = r;
785	    } else {
786		q = p;
787		p = p->next;
788	    }
789	}
790    }
791#else
792    (void) string;
793#endif
794}
795
796#ifdef NO_LEAKS
797void
798_dlg_inputstr_leaks(void)
799{
800#if USE_CACHING
801    dialog_state.finish_string = TRUE;
802    trace_cache(__FILE__, __LINE__);
803    while (cache_list != 0) {
804	dlg_finish_string(cache_list->string_at);
805    }
806#endif /* USE_CACHING */
807}
808#endif /* NO_LEAKS */
809