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 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 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 * 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 * 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 * 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 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 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 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 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 * 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 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 * 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 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 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 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 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 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 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 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 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