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