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
51 typedef 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
66 static CACHE *cache_list;
67
68 typedef enum {
69 cInxCols
70 ,cCntWideBytes
71 ,cCntWideChars
72 ,cInxWideChars
73 ,cMAX
74 } CACHE_USED;
75
76 #ifdef HAVE_TSEARCH
77 static void *sorted_cache;
78 #endif
79
80 #ifdef USE_WIDE_CURSES
81 static int
have_locale(void)82 have_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
102 static void
103 show_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
112 static void
113 trace_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
125 static int
compare_cache(const void * a,const void * b)126 compare_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
137 static CACHE *
find_cache(int cache_num,const char * string)138 find_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
165 static CACHE *
make_cache(int cache_num,const char * string)166 make_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
184 static CACHE *
load_cache(int cache_num,const char * string)185 load_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
195 static 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 */
204 static bool
same_cache2(CACHE * cache,const char * string,unsigned i_len)205 same_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 */
243 static bool
same_cache1(CACHE * cache,const char * string,size_t i_len)244 same_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
273 static int
dlg_count_wcbytes(const char * string,size_t len)274 dlg_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 */
309 int
dlg_count_wchars(const char * string)310 dlg_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 */
352 const int *
dlg_index_wchars(const char * string)353 dlg_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 */
389 int
dlg_find_index(const int * list,int limit,int to_find)390 dlg_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 */
406 const int *
dlg_index_columns(const char * string)407 dlg_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 */
486 int
dlg_count_columns(const char * string)487 dlg_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 */
506 int
dlg_limit_columns(const char * string,int limit,int offset)507 dlg_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 */
521 bool
dlg_edit_string(char * string,int * chr_offset,int key,int fkey,bool force)522 dlg_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
645 static void
compute_edit_offset(const char * string,int chr_offset,int x_last,int * p_dpy_column,int * p_scroll_amt)646 compute_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 */
680 int
dlg_edit_offset(char * string,int chr_offset,int x_last)681 dlg_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 */
694 void
dlg_show_string(WINDOW * win,const char * string,int chr_offset,chtype attr,int y_base,int x_base,int x_last,bool hidden,bool force)695 dlg_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 */
755 void
dlg_finish_string(const char * string)756 dlg_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
797 void
_dlg_inputstr_leaks(void)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