browser.c (22254B)
1 /************************************************************************** 2 * browser.c -- This file is part of GNU nano. * 3 * * 4 * Copyright (C) 2001-2011, 2013-2025 Free Software Foundation, Inc. * 5 * Copyright (C) 2015-2016, 2020, 2022 Benno Schulenberg * 6 * * 7 * GNU nano is free software: you can redistribute it and/or modify * 8 * it under the terms of the GNU General Public License as published * 9 * by the Free Software Foundation, either version 3 of the License, * 10 * or (at your option) any later version. * 11 * * 12 * GNU nano is distributed in the hope that it will be useful, * 13 * but WITHOUT ANY WARRANTY; without even the implied warranty * 14 * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * 15 * See the GNU General Public License for more details. * 16 * * 17 * You should have received a copy of the GNU General Public License * 18 * along with this program. If not, see https://gnu.org/licenses/. * 19 * * 20 **************************************************************************/ 21 22 #include "prototypes.h" 23 24 #ifdef ENABLE_BROWSER 25 26 #include <errno.h> 27 #include <stdint.h> 28 #include <string.h> 29 #include <unistd.h> 30 31 static char **filelist = NULL; 32 /* The list of files to display in the file browser. */ 33 static size_t list_length = 0; 34 /* The number of files in the list. */ 35 static size_t usable_rows = 0; 36 /* The number of screen rows we can use to display the list. */ 37 static int piles = 0; 38 /* The number of files that we can display per screen row. */ 39 static int gauge = 0; 40 /* The width of a 'pile' -- the widest filename plus ten. */ 41 static size_t selected = 0; 42 /* The currently selected filename in the list; zero-based. */ 43 44 /* Fill 'filelist' with the names of the files in the given directory, set 45 * 'list_length' to the number of names in that list, set 'gauge' to the 46 * width of the widest filename plus ten, and set 'piles' to the number of 47 * files that can be displayed per screen row. And sort the list too. */ 48 void read_the_list(const char *path, DIR *dir) 49 { 50 size_t path_len = strlen(path); 51 const struct dirent *entry; 52 size_t widest = 0; 53 size_t index = 0; 54 55 /* Find the width of the widest filename in the current folder. */ 56 while ((entry = readdir(dir)) != NULL) { 57 size_t span = breadth(entry->d_name); 58 59 if (span > widest) 60 widest = span; 61 62 index++; 63 } 64 65 /* Reserve ten columns for blanks plus file size. */ 66 gauge = widest + 10; 67 68 /* If needed, make room for ".. (parent dir)". */ 69 if (gauge < 15) 70 gauge = 15; 71 /* Make sure we're not wider than the window. */ 72 if (gauge > COLS) 73 gauge = COLS; 74 75 rewinddir(dir); 76 77 free_chararray(filelist, list_length); 78 79 list_length = index; 80 index = 0; 81 82 filelist = nmalloc(list_length * sizeof(char *)); 83 84 while ((entry = readdir(dir)) != NULL && index < list_length) { 85 /* Don't show the useless dot item. */ 86 if (strcmp(entry->d_name, ".") == 0) 87 continue; 88 89 filelist[index] = nmalloc(path_len + strlen(entry->d_name) + 1); 90 sprintf(filelist[index], "%s%s", path, entry->d_name); 91 92 index++; 93 } 94 95 /* Maybe the number of files in the directory decreased between 96 * the first time we scanned and the second time. */ 97 list_length = index; 98 99 /* Sort the list of names. */ 100 qsort(filelist, list_length, sizeof(char *), diralphasort); 101 102 /* Calculate how many files fit on a line -- feigning room for two 103 * spaces beyond the right edge, and adding two spaces of padding 104 * between columns. */ 105 piles = (COLS + 2) / (gauge + 2); 106 107 usable_rows = editwinrows - (ISSET(ZERO) && LINES > 1 ? 1 : 0); 108 } 109 110 /* Reselect the given file or directory name, if it still exists. */ 111 void reselect(const char *name) 112 { 113 size_t looking_at = 0; 114 115 while (looking_at < list_length && strcmp(filelist[looking_at], name) != 0) 116 looking_at++; 117 118 /* If the sought name was found, select it; otherwise, just move 119 * the highlight so that the changed selection will be noticed, 120 * but make sure to stay within the current available range. */ 121 if (looking_at < list_length) 122 selected = looking_at; 123 else if (selected > list_length) 124 selected = list_length - 1; 125 else 126 --selected; 127 } 128 129 /* Display at most a screenful of filenames from the gleaned filelist. */ 130 void browser_refresh(void) 131 { 132 int row = 0, col = 0; 133 /* The current row and column while the list is getting displayed. */ 134 int the_row = 0, the_column = 0; 135 /* The row and column of the selected item. */ 136 char *info; 137 /* The additional information that we'll display about a file. */ 138 139 titlebar(present_path); 140 blank_edit(); 141 142 for (size_t index = selected - selected % (usable_rows * piles); 143 index < list_length && row < usable_rows; index++) { 144 const char *thename = tail(filelist[index]); 145 /* The filename we display, minus the path. */ 146 size_t namelen = breadth(thename); 147 /* The length of the filename in columns. */ 148 size_t infolen; 149 /* The length of the file information in columns. */ 150 size_t infomaxlen = 7; 151 /* The maximum length of the file information in columns: 152 * normally seven, but will be twelve for "(parent dir)". */ 153 bool dots = (COLS >= 15 && namelen >= gauge - infomaxlen); 154 /* Whether to put an ellipsis before the filename? We don't 155 * waste space on dots when there are fewer than 15 columns. */ 156 char *disp = display_string(thename, dots ? 157 namelen + infomaxlen + 4 - gauge : 0, gauge, FALSE, FALSE); 158 /* The filename (or a fragment of it) in displayable format. 159 * When a fragment, account for dots plus one space padding. */ 160 struct stat state; 161 162 /* If this is the selected item, draw its highlighted bar upfront, and 163 * remember its location to be able to place the cursor on it. */ 164 if (index == selected) { 165 wattron(midwin, interface_color_pair[SELECTED_TEXT]); 166 mvwprintw(midwin, row, col, "%*s", gauge, " "); 167 the_row = row; 168 the_column = col; 169 } 170 171 /* If the name is too long, we display something like "...ename". */ 172 if (dots) 173 mvwaddstr(midwin, row, col, "..."); 174 mvwaddstr(midwin, row, dots ? col + 3 : col, disp); 175 176 col += gauge; 177 178 /* Show information about the file: "--" for symlinks (except when 179 * they point to a directory) and for files that have disappeared, 180 * "(dir)" for directories, and the file size for normal files. */ 181 if (lstat(filelist[index], &state) == -1 || S_ISLNK(state.st_mode)) { 182 if (stat(filelist[index], &state) == -1 || !S_ISDIR(state.st_mode)) 183 info = copy_of("--"); 184 else 185 /* TRANSLATORS: Anything more than 7 cells gets clipped. */ 186 info = copy_of(_("(dir)")); 187 } else if (S_ISDIR(state.st_mode)) { 188 if (strcmp(thename, "..") == 0) { 189 /* TRANSLATORS: Anything more than 12 cells gets clipped. */ 190 info = copy_of(_("(parent dir)")); 191 infomaxlen = 12; 192 } else 193 info = copy_of(_("(dir)")); 194 } else { 195 off_t result = state.st_size; 196 char modifier; 197 198 info = nmalloc(infomaxlen + 1); 199 200 /* Massage the file size into a human-readable form. */ 201 if (state.st_size < (1 << 10)) 202 modifier = ' '; /* bytes */ 203 else if (state.st_size < (1 << 20)) { 204 result >>= 10; 205 modifier = 'K'; /* kilobytes */ 206 } else if (state.st_size < (1 << 30)) { 207 result >>= 20; 208 modifier = 'M'; /* megabytes */ 209 } else { 210 result >>= 30; 211 modifier = 'G'; /* gigabytes */ 212 } 213 214 /* Show the size if less than a terabyte, else show "(huge)". */ 215 if (result < (1 << 10)) 216 sprintf(info, "%4ju %cB", (intmax_t)result, modifier); 217 else 218 /* TRANSLATORS: Anything more than 7 cells gets clipped. 219 * If necessary, you can leave out the parentheses. */ 220 info = mallocstrcpy(info, _("(huge)")); 221 } 222 223 /* Make sure info takes up no more than infomaxlen columns. */ 224 infolen = breadth(info); 225 if (infolen > infomaxlen) { 226 info[actual_x(info, infomaxlen)] = '\0'; 227 infolen = infomaxlen; 228 } 229 230 mvwaddstr(midwin, row, col - infolen, info); 231 232 /* If this is the selected item, finish its highlighting. */ 233 if (index == selected) 234 wattroff(midwin, interface_color_pair[SELECTED_TEXT]); 235 236 free(disp); 237 free(info); 238 239 /* Add some space between the columns. */ 240 col += 2; 241 242 /* If the next item will not fit on this row, move to next row. */ 243 if (col > COLS - gauge) { 244 row++; 245 col = 0; 246 } 247 } 248 249 /* If requested, put the cursor on the selected item and switch it on. */ 250 if (ISSET(SHOW_CURSOR)) { 251 wmove(midwin, the_row, the_column); 252 curs_set(1); 253 } 254 255 wnoutrefresh(midwin); 256 } 257 258 /* Look for the given needle in the list of files, forwards or backwards. */ 259 void findfile(const char *needle, bool forwards) 260 { 261 size_t began_at = selected; 262 263 /* Iterate through the list of filenames, until a match is found or 264 * we've come back to the point where we started. */ 265 while (TRUE) { 266 if (forwards) { 267 if (selected++ == list_length - 1) { 268 selected = 0; 269 statusbar(_("Search Wrapped")); 270 } 271 } else { 272 if (selected-- == 0) { 273 selected = list_length - 1; 274 statusbar(_("Search Wrapped")); 275 } 276 } 277 278 /* When the needle occurs in the basename of the file, we have a match. */ 279 if (mbstrcasestr(tail(filelist[selected]), needle)) { 280 if (selected == began_at) 281 statusbar(_("This is the only occurrence")); 282 return; 283 } 284 285 /* When we're back at the starting point without any match... */ 286 if (selected == began_at) { 287 not_found_msg(needle); 288 return; 289 } 290 } 291 } 292 293 /* Prepare the prompt and ask the user what to search for; then search for it. 294 * If forwards is TRUE, search forward in the list; otherwise, search backward. */ 295 void search_filename(bool forwards) 296 { 297 char *thedefault; 298 int response; 299 300 /* If something was searched for before, show it between square brackets. */ 301 if (*last_search != '\0') { 302 char *disp = display_string(last_search, 0, COLS / 3, FALSE, FALSE); 303 304 thedefault = nmalloc(strlen(disp) + 7); 305 /* We use (COLS / 3) here because we need to see more on the line. */ 306 sprintf(thedefault, " [%s%s]", disp, 307 (breadth(last_search) > COLS / 3) ? "..." : ""); 308 free(disp); 309 } else 310 thedefault = copy_of(""); 311 312 /* Now ask what to search for. */ 313 response = do_prompt(MWHEREISFILE, "", &search_history, 314 browser_refresh, "%s%s%s", _("Search"), 315 /* TRANSLATORS: A modifier of the Search prompt. */ 316 !forwards ? _(" [Backwards]") : "", thedefault); 317 free(thedefault); 318 319 /* If the user cancelled, or typed <Enter> on a blank answer and 320 * nothing was searched for yet during this session, get out. */ 321 if (response == -1 || (response == -2 && *last_search == '\0')) { 322 statusbar(_("Cancelled")); 323 return; 324 } 325 326 /* If the user typed an answer, remember it. */ 327 if (*answer != '\0') { 328 last_search = mallocstrcpy(last_search, answer); 329 #ifdef ENABLE_HISTORIES 330 update_history(&search_history, answer, PRUNE_DUPLICATE); 331 #endif 332 } 333 334 if (response == 0 || response == -2) 335 findfile(last_search, forwards); 336 } 337 338 /* Search again without prompting for the last given search string, 339 * either forwards or backwards. */ 340 void research_filename(bool forwards) 341 { 342 #ifdef ENABLE_HISTORIES 343 /* If nothing was searched for yet, take the last item from history. */ 344 if (*last_search == '\0' && searchbot->prev != NULL) 345 last_search = mallocstrcpy(last_search, searchbot->prev->data); 346 #endif 347 348 if (*last_search == '\0') 349 statusbar(_("No current search pattern")); 350 else { 351 wipe_statusbar(); 352 findfile(last_search, forwards); 353 } 354 } 355 356 /* Select the first file in the list -- called directly by ^W^Y. */ 357 void to_first_file(void) 358 { 359 selected = 0; 360 } 361 362 /* Select the last file in the list -- called directly by ^W^V. */ 363 void to_last_file(void) 364 { 365 selected = list_length - 1; 366 } 367 368 /* Strip one element from the end of path, and return the stripped path. 369 * The returned string is dynamically allocated, and should be freed. */ 370 char *strip_last_component(const char *path) 371 { 372 char *copy = copy_of(path); 373 char *last_slash = strrchr(copy, '/'); 374 375 if (last_slash != NULL) 376 *last_slash = '\0'; 377 378 return copy; 379 } 380 381 /* Allow the user to browse through the directories in the filesystem, 382 * starting at the given path. */ 383 char *browse(char *path) 384 { 385 char *present_name = NULL; 386 /* The name of the currently selected file, or of the directory we 387 * were in before backing up to "..". */ 388 size_t old_selected; 389 /* The number of the selected file before the current selected file. */ 390 DIR *dir; 391 /* The directory whose contents we are showing. */ 392 char *chosen = NULL; 393 /* The name of the file that the user picked, or NULL if none. */ 394 395 read_directory_contents: 396 /* We come here when the user refreshes or selects a new directory. */ 397 398 path = free_and_assign(path, get_full_path(path)); 399 400 if (path != NULL) 401 dir = opendir(path); 402 403 if (path == NULL || dir == NULL) { 404 statusline(ALERT, _("Cannot open directory: %s"), strerror(errno)); 405 /* If we don't have a file list, there is nothing to show. */ 406 if (filelist == NULL) { 407 lastmessage = VACUUM; 408 free(present_name); 409 free(path); 410 napms(1200); 411 return NULL; 412 } 413 path = mallocstrcpy(path, present_path); 414 present_name = mallocstrcpy(present_name, filelist[selected]); 415 } 416 417 if (dir != NULL) { 418 /* Get the file list, and set gauge and piles in the process. */ 419 read_the_list(path, dir); 420 closedir(dir); 421 dir = NULL; 422 } 423 424 /* If something was selected before, reselect it; 425 * otherwise, just select the first item (..). */ 426 if (present_name != NULL) { 427 reselect(present_name); 428 free(present_name); 429 present_name = NULL; 430 } else 431 selected = 0; 432 433 old_selected = (size_t)-1; 434 435 present_path = mallocstrcpy(present_path, path); 436 437 titlebar(path); 438 439 if (list_length == 0) { 440 statusline(ALERT, _("No entries")); 441 napms(1200); 442 } else while (TRUE) { 443 functionptrtype function; 444 int kbinput; 445 446 lastmessage = VACUUM; 447 448 bottombars(MBROWSER); 449 450 /* Display (or redisplay) the file list if the list itself or 451 * the selected file has changed. */ 452 if (old_selected != selected || ISSET(SHOW_CURSOR)) 453 browser_refresh(); 454 455 old_selected = selected; 456 457 kbinput = get_kbinput(midwin, ISSET(SHOW_CURSOR)); 458 459 #ifdef ENABLE_MOUSE 460 if (kbinput == KEY_MOUSE) { 461 int mouse_x, mouse_y; 462 463 /* When the user clicked in the file list, select a filename. */ 464 if (get_mouseinput(&mouse_y, &mouse_x, TRUE) == 0 && 465 wmouse_trafo(midwin, &mouse_y, &mouse_x, FALSE)) { 466 selected = selected - selected % (usable_rows * piles) + 467 (mouse_y * piles) + (mouse_x / (gauge + 2)); 468 469 /* When beyond end-of-row, select the preceding filename. */ 470 if (mouse_x > piles * (gauge + 2)) 471 selected--; 472 473 /* When beyond end-of-list, select the last filename. */ 474 if (selected > list_length - 1) 475 selected = list_length - 1; 476 477 /* When a filename is clicked a second time, choose it. */ 478 if (old_selected == selected) 479 kbinput = KEY_ENTER; 480 } 481 482 if (kbinput == KEY_MOUSE) 483 continue; 484 } 485 #endif /* ENABLE_MOUSE */ 486 487 function = interpret(kbinput); 488 489 if (function == do_help || function == full_refresh) { 490 function(); 491 #ifndef NANO_TINY 492 /* Simulate a terminal resize to force a directory reread, 493 * or because the terminal dimensions might have changed. */ 494 kbinput = THE_WINDOW_RESIZED; 495 } else if (function == do_toggle && get_shortcut(kbinput)->toggle == NO_HELP) { 496 TOGGLE(NO_HELP); 497 window_init(); 498 kbinput = THE_WINDOW_RESIZED; 499 #endif 500 } else if (function == do_search_backward) { 501 search_filename(BACKWARD); 502 } else if (function == do_search_forward) { 503 search_filename(FORWARD); 504 } else if (function == do_findprevious) { 505 research_filename(BACKWARD); 506 } else if (function == do_findnext) { 507 research_filename(FORWARD); 508 } else if (function == do_left) { 509 if (selected > 0) 510 selected--; 511 } else if (function == do_right) { 512 if (selected < list_length - 1) 513 selected++; 514 } else if (function == to_prev_word) { 515 selected -= (selected % piles); 516 } else if (function == to_next_word) { 517 selected += piles - 1 - (selected % piles); 518 if (selected >= list_length) 519 selected = list_length - 1; 520 } else if (function == do_up) { 521 if (selected >= piles) 522 selected -= piles; 523 } else if (function == do_down) { 524 if (selected + piles <= list_length - 1) 525 selected += piles; 526 } else if (function == to_prev_block) { 527 selected = ((selected / (usable_rows * piles)) * usable_rows * piles) + 528 selected % piles; 529 } else if (function == to_next_block) { 530 selected = ((selected / (usable_rows * piles)) * usable_rows * piles) + 531 selected % piles + usable_rows * piles - piles; 532 if (selected >= list_length) 533 selected = (list_length / piles) * piles + selected % piles; 534 if (selected >= list_length) 535 selected -= piles; 536 } else if (function == do_page_up) { 537 if (selected < piles) 538 selected = 0; 539 else if (selected < usable_rows * piles) 540 selected = selected % piles; 541 else 542 selected -= usable_rows * piles; 543 } else if (function == do_page_down) { 544 if (selected + piles >= list_length - 1) 545 selected = list_length - 1; 546 else if (selected + usable_rows * piles >= list_length) 547 selected = (selected + usable_rows * piles - list_length) % piles + 548 list_length - piles; 549 else 550 selected += usable_rows * piles; 551 } else if (function == to_first_file || function == to_last_file) { 552 function(); 553 } else if (function == goto_dir) { 554 /* Ask for the directory to go to. */ 555 if (do_prompt(MGOTODIR, "", NULL, 556 /* TRANSLATORS: This is a prompt. */ 557 browser_refresh, _("Go To Directory")) < 0) { 558 statusbar(_("Cancelled")); 559 continue; 560 } 561 562 path = free_and_assign(path, real_dir_from_tilde(answer)); 563 564 /* If the given path is relative, join it with the current path. */ 565 if (*path != '/') { 566 path = nrealloc(path, strlen(present_path) + strlen(answer) + 1); 567 sprintf(path, "%s%s", present_path, answer); 568 } 569 570 #ifdef ENABLE_OPERATINGDIR 571 if (outside_of_confinement(path, FALSE)) { 572 /* TRANSLATORS: This refers to the confining effect of 573 * the option --operatingdir, not of --restricted. */ 574 statusline(ALERT, _("Can't go outside of %s"), operating_dir); 575 path = mallocstrcpy(path, present_path); 576 continue; 577 } 578 #endif 579 /* Snip any trailing slashes, so the name can be compared. */ 580 while (strlen(path) > 1 && path[strlen(path) - 1] == '/') 581 path[strlen(path) - 1] = '\0'; 582 583 /* In case the specified directory cannot be entered, select it 584 * (if it is in the current list) so it will be highlighted. */ 585 for (size_t j = 0; j < list_length; j++) 586 if (strcmp(filelist[j], path) == 0) 587 selected = j; 588 589 /* Try opening and reading the specified directory. */ 590 goto read_directory_contents; 591 } else if (function == do_enter) { 592 struct stat st; 593 594 /* It isn't possible to move up from the root directory. */ 595 if (strcmp(filelist[selected], "/..") == 0) { 596 statusline(ALERT, _("Can't move up a directory")); 597 continue; 598 } 599 600 #ifdef ENABLE_OPERATINGDIR 601 /* Note: The selected file can be outside the operating 602 * directory if it's ".." or if it's a symlink to a 603 * directory outside the operating directory. */ 604 if (outside_of_confinement(filelist[selected], FALSE)) { 605 statusline(ALERT, _("Can't go outside of %s"), operating_dir); 606 continue; 607 } 608 #endif 609 /* If for some reason the file is inaccessible, complain. */ 610 if (stat(filelist[selected], &st) == -1) { 611 statusline(ALERT, _("Error reading %s: %s"), 612 filelist[selected], strerror(errno)); 613 continue; 614 } 615 616 /* If it isn't a directory, a file was selected -- we're done. */ 617 if (!S_ISDIR(st.st_mode)) { 618 chosen = copy_of(filelist[selected]); 619 break; 620 } 621 622 /* If we are moving up one level, remember where we came from, so 623 * this directory can be highlighted and easily reentered. */ 624 if (strcmp(tail(filelist[selected]), "..") == 0) 625 present_name = strip_last_component(filelist[selected]); 626 627 /* Try opening and reading the selected directory. */ 628 path = mallocstrcpy(path, filelist[selected]); 629 goto read_directory_contents; 630 #ifdef ENABLE_NANORC 631 } else if (function == (functionptrtype)implant) { 632 implant(first_sc_for(MBROWSER, function)->expansion); 633 #endif 634 #ifndef NANO_TINY 635 } else if (kbinput == START_OF_PASTE) { 636 while (get_kbinput(midwin, BLIND) != END_OF_PASTE) 637 ; 638 statusline(AHEM, _("Paste is ignored")); 639 } else if (kbinput == THE_WINDOW_RESIZED) { 640 ; /* Gets handled below. */ 641 #endif 642 } else if (function == do_exit) { 643 break; 644 } else 645 unbound_key(kbinput); 646 647 #ifndef NANO_TINY 648 /* If the terminal resized (or might have), refresh the file list. */ 649 if (kbinput == THE_WINDOW_RESIZED) { 650 /* Remember the selected file, to be able to reselect it. */ 651 present_name = copy_of(filelist[selected]); 652 goto read_directory_contents; 653 } 654 #endif 655 } 656 657 titlebar(NULL); 658 edit_refresh(); 659 660 free(path); 661 662 free_chararray(filelist, list_length); 663 filelist = NULL; 664 list_length = 0; 665 666 return chosen; 667 } 668 669 /* Prepare to start browsing. If the given path has a directory part, 670 * start browsing in that directory, otherwise in the current directory. */ 671 char *browse_in(const char *inpath) 672 { 673 char *path = real_dir_from_tilde(inpath); 674 struct stat fileinfo; 675 676 /* If path is not a directory, try to strip a filename from it; if then 677 * still not a directory, use the current working directory instead. */ 678 if (stat(path, &fileinfo) == -1 || !S_ISDIR(fileinfo.st_mode)) { 679 path = free_and_assign(path, strip_last_component(path)); 680 681 if (stat(path, &fileinfo) == -1 || !S_ISDIR(fileinfo.st_mode)) { 682 path = free_and_assign(path, realpath(".", NULL)); 683 684 if (path == NULL) { 685 statusline(ALERT, _("The working directory has disappeared")); 686 napms(1200); 687 return NULL; 688 } 689 } 690 } 691 692 #ifdef ENABLE_OPERATINGDIR 693 /* If the resulting path isn't in the operating directory, 694 * use the operating directory instead. */ 695 if (outside_of_confinement(path, FALSE)) 696 path = mallocstrcpy(path, operating_dir); 697 #endif 698 699 return browse(path); 700 } 701 702 #endif /* ENABLE_BROWSER */