File: bf.cpp 1 /* 2 The MIT License (MIT) 3 4 Copyright © 2020-2025 pacman64 5 6 Permission is hereby granted, free of charge, to any person obtaining a copy of 7 this software and associated documentation files (the “Software”), to deal 8 in the Software without restriction, including without limitation the rights to 9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 10 of the Software, and to permit persons to whom the Software is furnished to do 11 so, subject to the following conditions: 12 13 The above copyright notice and this permission notice shall be included in all 14 copies or substantial portions of the Software. 15 16 THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 SOFTWARE. 23 */ 24 25 /* 26 You can build this command-line app by running 27 28 c++ -Wall -s -O3 -march=native -mtune=native -flto -o bf bf.cpp -lncursesw 29 */ 30 31 #include <algorithm> 32 #include <cstring> 33 #include <dirent.h> 34 #include <fstream> 35 #include <locale.h> 36 #include <ncurses.h> 37 #include <stdio.h> 38 #include <stdlib.h> 39 #include <string> 40 #include <unistd.h> 41 #include <vector> 42 #include <sys/stat.h> 43 44 #ifdef RED_ERRORS 45 #define ERROR_STYLE "\x1b[38;2;204;0;0m" 46 #ifdef __APPLE__ 47 #define ERROR_STYLE "\x1b[31m" 48 #endif 49 #define RESET_STYLE "\x1b[0m" 50 #else 51 #define ERROR_STYLE "" 52 #define RESET_STYLE "" 53 #endif 54 55 #define ERROR_LINE(MSG) (ERROR_STYLE MSG RESET_STYLE "\n") 56 57 #define NO_CHOICE 1 58 #define BAD_ALLOC 2 59 #define BAD_ARGS 3 60 #define BAD_TTY 4 61 #define NOT_A_FILE 5 62 63 #ifndef IBUF_SIZE 64 #define IBUF_SIZE (32 * 1024) 65 #endif 66 67 #define FILE_SIZE_ROOM 16 68 69 #ifndef MAX_LINES 70 #define MAX_LINES (500 * 1000) 71 #endif 72 73 #ifndef MAX_BYTES 74 #define MAX_BYTES (500 * 1024 * 1024) 75 #endif 76 77 using namespace std; 78 79 vector<string> help = { 80 "bf [option...] [folder...]", 81 "", 82 "", 83 "Browse Folders is a text user-interface (TUI) to do just that. By default", 84 "it starts browsing from the current folder, but you can choose a different", 85 "starting point as an optional cmd-line argument when starting this script.", 86 "", 87 "", 88 " Enter Quit this app, emitting the currently-selected entry", 89 " Escape Quit this app without emitting an entry; quit text viewers", 90 " F1 Toggle help-message screen; the Escape key also quits it", 91 " F5 Update current-folder entries, in case they've changed", 92 " F6 Toggle size-sorting mode, and update current-folder entries", 93 " F10 Quit this app without emitting an entry; quit text viewers", 94 " F12 Quit this app without emitting an entry; quit text viewers", 95 "", 96 " Left Go to the current folder's parent folder", 97 " Right Go to the currently-selected folder", 98 " Backspace Go to the current folder's parent folder", 99 " Tab Go to the currently-selected folder", 100 "", 101 " Home Select the first entry in the current folder", 102 " End Select the last entry in the current folder", 103 " Up Select the entry before the currently selected one", 104 " Down Select the entry after the currently selected one", 105 " Page Up Select entry by jumping one screen backward", 106 " Page Down Select entry by jumping one screen forward", 107 "", 108 " [a-z0-9] Select next entry starting with letter/digit", 109 "", 110 "", 111 "Escape quits the app without emitting the currently-selected item and with", 112 "an error-code, while Enter emits the selected item, quitting successfully.", 113 "", 114 "Folders are shown without a file-size, and are always shown before files.", 115 "", 116 "Some file/folder entries may be special and/or give an error when queried", 117 "for their file-size: these are shown with a question mark where their size", 118 "would normally be.", 119 "", 120 "Entering text files lets you view and scroll their lines up and down.", 121 "", 122 "The right side of the screen also shows little symbols when there are more", 123 "entries before/after the ones currently showing.", 124 "", 125 "The only options are:", 126 "", 127 " -c, --c emit file contents instead of file/folder paths", 128 " -f, --f emit file contents instead of file/folder paths", 129 "", 130 " -h, --h show this help message and quit", 131 " -help, --help show this help message and quit", 132 "", 133 "You can also view this help message by pressing the F1 key while browsing", 134 "folders: the help viewer works the same as the text-file viewer; you can", 135 "quit the help screen with the Escape key, or with the F1 key.", 136 "", 137 "When things have changed in the current folder, you can press the F5 key", 138 "to reload the entries on screen, so there's no need to manually get out", 139 "and back into the current folder as a workaround.", 140 "", 141 "When given a file name, instead of a starting folder, this app tries to", 142 "load it as plain-text, letting you scroll up/down its lines, unless the", 143 "file is too big or has too many lines.", 144 }; 145 146 bool ends_with(const char* s, char what) { 147 if (s == NULL) { 148 return false; 149 } 150 151 char last = 0; 152 for (; *s != 0; s++) { 153 last = *s; 154 } 155 return last == what; 156 } 157 158 char* clone(const char* s) { 159 if (s == NULL) { 160 return NULL; 161 } 162 163 const auto n = strlen(s); 164 char* p = static_cast<char*>(malloc(n + 1)); 165 if (p != NULL) { 166 strcpy(p, s); 167 } 168 return p; 169 } 170 171 bool entry_info(const char* path, size_t* size, bool* is_file) { 172 struct stat st; 173 if (stat(path, &st) != 0) { 174 return false; 175 } 176 177 if (S_ISDIR(st.st_mode)) { 178 *size = 0; 179 *is_file = false; 180 return true; 181 } 182 *is_file = true; 183 *size = st.st_size; 184 return true; 185 } 186 187 bool is_folder(const char* path) { 188 size_t size = 0; 189 bool is_file = false; 190 return entry_info(path, &size, &is_file) && !is_file; 191 } 192 193 struct entry { 194 char* name; // relative name of file/folder 195 ssize_t size; // file-size, or negative value if entry is a folder 196 }; 197 198 struct browser { 199 char* folder; // full path of the current folder 200 vector<entry> entries; // cache of the current/latest folder entries 201 vector<char*> stack; // history of folder names up to the current one 202 203 char* result; // the filepath picked, if any 204 205 size_t width; // current number of visible cells per line in terminal 206 size_t height; // current number of visible lines in terminal 207 208 size_t top; // index of the top entry showing on screen 209 size_t selection; // index of the highlighted entry 210 211 int exit_code; 212 213 bool sort_size; // whether to reverse-sort file entries by size 214 bool help_mode; // whether currently showing the help screen 215 }; 216 217 bool change_folder(browser& b, const char* path) { 218 if (chdir(path) != 0) { 219 return false; 220 } 221 222 char name[PATH_MAX]; 223 if (getcwd(name, sizeof(name)) == NULL) { 224 return false; 225 } 226 227 free(b.folder); 228 b.folder = clone(name); 229 return true; 230 } 231 232 ssize_t find_name(const browser& b, const char* name) { 233 for (size_t i = 0; i < b.entries.size(); i++) { 234 if (strcmp(name, b.entries[i].name)) { 235 return i; 236 } 237 } 238 return -1; 239 } 240 241 // find the index of the top item which would show on the last page 242 ssize_t max_top(const browser& b, size_t length) { 243 if (length == 0 || length + 1 <= b.height) { 244 return 0; 245 } 246 return length - (b.height - 1); 247 } 248 249 bool isalphanum(int n) { 250 return false || 251 ('A' <= n && n <= 'Z') || 252 ('a' <= n && n <= 'z') || 253 ('0' <= n && n <= '9'); 254 } 255 256 ssize_t seek_initial_next(const browser& b, char lead, ssize_t start) { 257 const ssize_t n = b.entries.size(); 258 if (start < 0) { 259 start = -1; 260 } 261 if (start >= n) { 262 start = -1; 263 } 264 lead = tolower(lead); 265 266 for (ssize_t i = start + 1; i < n; i++) { 267 const char* s = b.entries[i].name; 268 if (tolower(s[0]) == lead) { 269 return i; 270 } 271 } 272 273 for (ssize_t i = 0; i < start; i++) { 274 const char* s = b.entries[i].name; 275 if (tolower(s[0]) == lead) { 276 return i; 277 } 278 } 279 280 return -1; 281 } 282 283 void write_spaces(ssize_t n) { 284 static const char spaces[256] = { 285 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 286 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 287 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 288 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 289 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 290 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 291 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 292 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 293 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 294 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 295 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 296 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 297 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 298 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 299 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 300 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 301 }; 302 303 const ssize_t k = sizeof(spaces); 304 for (; n >= k; n -= k) { 305 addnstr(spaces, k); 306 } 307 if (n > 0) { 308 addnstr(spaces, n); 309 } 310 } 311 312 // show a file size using commas between thousands, millions, etc. 313 void write_commas_uint(size_t n) { 314 size_t digits = 0; 315 // 20 is the most digits unsigned 64-bit ints can ever need 316 char buf[24]; 317 if (n == 0) { 318 buf[sizeof(buf) - 1] = '0'; 319 digits++; 320 } 321 for (; n > 0; digits++, n /= 10) { 322 buf[sizeof(buf) - 1 - digits] = (n % 10) + '0'; 323 } 324 325 // 32 is more than 20 digits and 6 commas can ever need 326 char result[32]; 327 // now emit the leading digits, which will be fewer than 3 328 size_t leading = digits % 3; 329 char* start = buf + sizeof(buf) - digits; 330 memcpy(result, start, leading); 331 start += leading; 332 digits -= leading; 333 334 // now the number of digits left is a multiple of 3 335 size_t i = leading; 336 for (; digits > 0; start += 3, digits -= 3) { 337 if (i > 0) { 338 result[i++] = ','; 339 } 340 result[i++] = start[0]; 341 result[i++] = start[1]; 342 result[i++] = start[2]; 343 } 344 345 // result[i] = 0; 346 write_spaces(FILE_SIZE_ROOM - i); 347 addnstr(result, i); 348 } 349 350 // forward-sort file entries by name 351 void sort_names(vector<entry>& files) { 352 sort(files.begin(), files.end(), [](const auto& x, const auto& y) { 353 return strcmp(x.name, y.name) < 0; 354 }); 355 } 356 357 // backward-sort file entries by size 358 void sort_sizes(vector<entry>& files) { 359 sort(files.begin(), files.end(), [](const auto& x, const auto& y) { 360 return y.size < x.size; 361 }); 362 } 363 364 void hightlight_top(const browser& b, const char* s, size_t len) { 365 if (len == 0) { 366 len = strlen(s); 367 } 368 if (len > b.width) { 369 len = b.width; 370 } 371 372 move(0, 0); 373 write_spaces(b.width); 374 375 move(0, 0); 376 attron(A_REVERSE); 377 addnstr(s, len); 378 attroff(A_REVERSE); 379 } 380 381 // update folder entries, without showing anything; file entries are sorted 382 void update_entries(browser& b, char* folder, DIR* entries) { 383 string full_path; 384 vector<char*> folders; 385 vector<entry> files; 386 387 while (true) { 388 const auto item = readdir(entries); 389 if (item == NULL) { 390 break; 391 } 392 393 const auto name = item->d_name; 394 395 // ignore entries `.` and `..` 396 if (name[0] == '.') { 397 if ((name[1] == 0) || (name[1] == '.' && name[2] == 0)) { 398 continue; 399 } 400 } 401 402 full_path.clear(); 403 full_path.append(folder); 404 full_path.push_back('/'); 405 full_path.append(name); 406 407 size_t size = 0; 408 bool is_file = false; 409 if (!entry_info(full_path.c_str(), &size, &is_file)) { 410 continue; 411 } 412 413 if (is_file) { 414 files.push_back(entry{clone(name), (ssize_t)size}); 415 } else { 416 folders.push_back(clone(name)); 417 } 418 } 419 420 sort(folders.begin(), folders.end(), [](const auto& x, const auto& y) { 421 return strcmp(x, y) < 0; 422 }); 423 424 if (b.sort_size) { 425 sort_sizes(files); 426 } else { 427 sort_names(files); 428 } 429 430 b.entries.clear(); 431 b.entries.reserve(folders.size() + files.size()); 432 for (const auto& s : folders) { 433 b.entries.push_back(entry{s, -1}); 434 } 435 b.entries.insert(b.entries.end(), files.begin(), files.end()); 436 } 437 438 void show_entry(const entry& e, size_t n) { 439 if (e.size >= 0) { 440 write_commas_uint(e.size); 441 write_spaces(2); 442 } else { 443 write_spaces(FILE_SIZE_ROOM + 2); 444 } 445 addnstr(e.name, n - FILE_SIZE_ROOM - 2); 446 } 447 448 void update_arrows(const browser& b, ssize_t top, size_t n) { 449 if (b.width < 2 || b.height < 1) { 450 return; 451 } 452 453 move(0, b.width - 1); 454 addstr((top > 0) ? 455 static_cast<const char*>("▲") : 456 static_cast<const char*>(" ")); 457 458 move(b.height - 1, b.width - 1); 459 addstr((top + b.height - 1 < n) ? 460 static_cast<const char*>("▼") : 461 static_cast<const char*>(" ")); 462 } 463 464 // show a screenful of folder entries from scratch, clearing previously-shown 465 // contents 466 void show_entries(browser& b) { 467 const auto w = b.width; 468 const auto h = b.height; 469 const auto max_w = w - 2; 470 const auto n = b.entries.size(); 471 472 clear(); 473 474 move(0, 0); 475 addnstr(b.folder, max_w); 476 477 for (size_t i = b.top, row = 1; i < n && row < h; i++, row++) { 478 move(row, 0); 479 if (i - b.top == b.selection) { 480 attron(A_REVERSE); 481 show_entry(b.entries[i], max_w); 482 attroff(A_REVERSE); 483 } else { 484 show_entry(b.entries[i], max_w); 485 } 486 } 487 488 update_arrows(b, b.top, b.entries.size()); 489 } 490 491 bool handle_folder(browser& b) { 492 if (b.folder == NULL) { 493 return false; 494 } 495 496 DIR* entries = opendir(b.folder); 497 if (entries == NULL) { 498 return false; 499 } 500 update_entries(b, b.folder, entries); 501 closedir(entries); 502 503 show_entries(b); 504 refresh(); 505 return true; 506 } 507 508 bool is_selection_showing(const browser& b, ssize_t i) { 509 return i >= (ssize_t)b.top && (size_t)i < b.top + b.height - 1; 510 } 511 512 void show_plain_entry(const browser& b, ssize_t i) { 513 if (!is_selection_showing(b, i)) { 514 return; 515 } 516 517 move(i + 1 - b.top, 0); 518 write_spaces(b.width); 519 520 const auto& e = b.entries[i]; 521 move(i + 1 - b.top, 0); 522 if (e.size >= 0) { 523 write_commas_uint(e.size); 524 write_spaces(2); 525 } else { 526 write_spaces(FILE_SIZE_ROOM + 2); 527 } 528 addnstr(e.name, b.width - FILE_SIZE_ROOM - 2 - 2); 529 } 530 531 void show_selected_entry(const browser& b, ssize_t i) { 532 if (!is_selection_showing(b, i)) { 533 return; 534 } 535 536 move(i + 1 - b.top, 0); 537 write_spaces(b.width); 538 539 const auto& e = b.entries[i]; 540 move(i + 1 - b.top, 0); 541 attron(A_REVERSE); 542 if (e.size >= 0) { 543 write_commas_uint(e.size); 544 write_spaces(2); 545 } else { 546 write_spaces(FILE_SIZE_ROOM + 2); 547 } 548 addnstr(e.name, b.width - FILE_SIZE_ROOM - 2 - 2); 549 attroff(A_REVERSE); 550 } 551 552 // calculate the index of the top item of the currently showing page 553 size_t get_page_top(const browser& b, ssize_t i) { 554 if (0 <= i && (size_t)i < b.entries.size()) { 555 return i - (i % (b.height - 1)); 556 } 557 return -1; 558 } 559 560 // change currently selected item among folder entries 561 void handle_select(browser& b, ssize_t i) { 562 const auto n = b.entries.size(); 563 if (n == 0) { 564 return; 565 } 566 567 if (i < 0) { 568 i = n - 1; 569 } else if (i >= (ssize_t)n) { 570 i = 0; 571 } 572 573 const auto p1 = get_page_top(b, b.selection); 574 const auto p2 = get_page_top(b, i); 575 576 if (p1 == p2) { 577 show_plain_entry(b, b.selection); 578 show_selected_entry(b, i); 579 b.selection = i; 580 } else { 581 b.top = p2; 582 b.selection = i; 583 show_entries(b); 584 show_selected_entry(b, i); 585 } 586 587 update_arrows(b, b.top, b.entries.size()); 588 refresh(); 589 } 590 591 void handle_page_change(browser& b, ssize_t i) { 592 const auto n = b.entries.size(); 593 if (n == 0) { 594 return; 595 } 596 597 if (i < 0) { 598 i = 0; 599 } 600 601 handle_select(b, get_page_top(b, i)); 602 } 603 604 void pick(browser& b) { 605 free(b.result); 606 b.exit_code = 0; 607 608 if (b.selection < b.entries.size()) { 609 b.result = clone(b.entries[b.selection].name); 610 } else { 611 b.result = clone(b.folder); 612 } 613 } 614 615 void unpick(browser& b) { 616 free(b.result); 617 b.result = NULL; 618 b.exit_code = NO_CHOICE; 619 } 620 621 // remove leading UTF-8 BOM bytes if present 622 void de_bom(string &s) { 623 const auto n = s.size(); 624 if (n >= 3 && s[0] == '\xef' && s[1] == '\xbb' && s[2] == '\xbf') { 625 s.erase(0, 3); 626 } 627 } 628 629 // remove trailing carriage-return byte if present 630 void de_cr(string &s) { 631 s.erase(remove(s.begin(), s.end(), '\r'), s.end()); 632 } 633 634 bool load_lines(const char* path, vector<string>& lines) { 635 size_t bytes; 636 bool is_file = false; 637 if (!entry_info(path, &bytes, &is_file)) { 638 return false; 639 } 640 if (!is_file || bytes > MAX_BYTES) { 641 return false; 642 } 643 644 string line; 645 ifstream r(path); 646 if (!r.is_open()) { 647 return false; 648 } 649 650 if (!getline(r, line)) { 651 r.close(); 652 return true; 653 } 654 655 de_bom(line); 656 de_cr(line); 657 lines.push_back(line); 658 659 // try to load everything, but give up after loading too many lines 660 for (size_t n = 1; getline(r, line) && n <= MAX_LINES; n++) { 661 de_cr(line); 662 lines.push_back(line); 663 } 664 665 r.close(); 666 return true; 667 } 668 669 void show_lines(const browser& b, vector<string>& lines, size_t top) { 670 const auto w = b.width; 671 const auto h = b.height; 672 const auto max_w = w; 673 const size_t n = lines.size(); 674 675 for (size_t i = top, row = 1; i < n && row < h; i++, row++) { 676 move(row, 0); 677 write_spaces(max_w); 678 move(row, 0); 679 addnstr(lines[i].c_str(), max_w); 680 } 681 682 update_arrows(b, top, n); 683 } 684 685 bool view_help(browser& b); 686 687 bool view_lines(browser& b, const string& title, vector<string>& lines) { 688 ssize_t top = 0; 689 const char* top_title = title.c_str(); 690 691 clear(); 692 hightlight_top(b, top_title, title.size()); 693 show_lines(b, lines, top); 694 refresh(); 695 696 while (true) { 697 const auto got = getch(); 698 699 switch (got) { 700 case '\x1b': 701 if (b.help_mode) { 702 b.help_mode = false; 703 return true; 704 } 705 unpick(b); 706 return false; 707 708 case KEY_F(10): 709 case KEY_F(12): 710 unpick(b); 711 return false; 712 713 case '\n': 714 pick(b); 715 return false; 716 717 case KEY_HOME: 718 if (top != 0) { 719 top = 0; 720 show_lines(b, lines, top); 721 refresh(); 722 } 723 continue; 724 725 case KEY_END: 726 if (top != max_top(b, lines.size())) { 727 top = max_top(b, lines.size()); 728 show_lines(b, lines, top); 729 refresh(); 730 } 731 continue; 732 733 case KEY_LEFT: 734 case KEY_BACKSPACE: 735 return true; 736 737 case KEY_UP: 738 if (top > 0) { 739 top--; 740 show_lines(b, lines, top); 741 refresh(); 742 } 743 continue; 744 745 case KEY_DOWN: 746 if (top < max_top(b, lines.size())) { 747 top++; 748 show_lines(b, lines, top); 749 refresh(); 750 } 751 continue; 752 753 case KEY_PPAGE: 754 if (top < ssize_t(b.height - 1)) { 755 top = 0; 756 } else { 757 top -= b.height - 1; 758 } 759 show_lines(b, lines, top); 760 refresh(); 761 continue; 762 763 case KEY_NPAGE: 764 if (top + ssize_t(b.height - 1) < max_top(b, lines.size())) { 765 top += b.height - 1; 766 } else { 767 top = max_top(b, lines.size()); 768 } 769 show_lines(b, lines, top); 770 refresh(); 771 continue; 772 773 case KEY_F(1): 774 if (b.help_mode) { 775 return true; 776 } 777 if (!view_help(b)) { 778 return false; 779 } 780 clear(); 781 hightlight_top(b, top_title, title.size()); 782 show_lines(b, lines, top); 783 refresh(); 784 continue; 785 786 case KEY_RESIZE: 787 clear(); 788 hightlight_top(b, top_title, title.size()); 789 show_lines(b, lines, top); 790 refresh(); 791 continue; 792 } 793 } 794 } 795 796 bool view_text_file(browser& b, const char* path) { 797 vector<string> lines; 798 if (!load_lines(path, lines)) { 799 return true; 800 } 801 802 string s; 803 s.append(b.folder); 804 if (!ends_with(b.folder, '/')) { 805 s.push_back('/'); 806 } 807 s.append(path); 808 return view_lines(b, s, lines); 809 } 810 811 bool view_help(browser& b) { 812 if (b.help_mode) { 813 return true; 814 } 815 816 b.help_mode = true; 817 const string help_title = "Help for Browse Folders (bf)"; 818 const auto ok = view_lines(b, help_title, help); 819 b.help_mode = false; 820 return ok; 821 } 822 823 bool dig_in(browser& b) { 824 const char* path = b.entries[b.selection].name; 825 826 if (!is_folder(path)) { 827 if (!view_text_file(b, path)) { 828 return false; 829 } 830 return handle_folder(b); 831 } 832 833 if (!change_folder(b, path)) { 834 return true; 835 } 836 837 b.selection = 0; 838 b.stack.push_back(b.folder); 839 return handle_folder(b); 840 } 841 842 bool back_out(browser& b) { 843 if (!change_folder(b, "..")) { 844 return true; 845 } 846 if (b.stack.size() > 0) { 847 b.stack.pop_back(); 848 } 849 const auto n = b.stack.size(); 850 b.selection = (n > 0) ? find_name(b, b.stack[n - 1]) : -1; 851 return handle_folder(b); 852 } 853 854 void bf(browser& b, entry start) { 855 if (start.size >= 0) { 856 if (!change_folder(b, ".")) { 857 b.exit_code = BAD_ARGS; 858 return; 859 } 860 861 if (view_text_file(b, start.name)) { 862 b.result = clone(start.name); 863 } 864 b.exit_code = (b.result == NULL) ? NO_CHOICE : 0; 865 return; 866 } 867 868 if (!change_folder(b, start.name)) { 869 b.exit_code = BAD_ARGS; 870 return; 871 } 872 b.stack.push_back(b.folder); 873 874 if (!handle_folder(b)) { 875 return; 876 } 877 878 while (true) { 879 const auto got = getch(); 880 881 switch (got) { 882 case KEY_RESIZE: 883 getmaxyx(NULL, b.height, b.width); 884 show_entries(b); 885 refresh(); 886 continue; 887 888 case '\n': 889 pick(b); 890 return; 891 892 case '\x1b': 893 case KEY_F(10): 894 case KEY_F(12): 895 unpick(b); 896 return; 897 898 case KEY_RIGHT: 899 case '\t': 900 if (!dig_in(b)) { 901 return; 902 } 903 continue; 904 905 case KEY_LEFT: 906 case KEY_BACKSPACE: 907 if (!back_out(b)) { 908 return; 909 } 910 continue; 911 912 case KEY_UP: 913 handle_select(b, b.selection - 1); 914 continue; 915 916 case KEY_DOWN: 917 handle_select(b, b.selection + 1); 918 continue; 919 920 case KEY_PPAGE: 921 handle_page_change(b, b.selection - (b.height - 1)); 922 continue; 923 924 case KEY_NPAGE: 925 handle_page_change(b, b.selection + (b.height - 1)); 926 continue; 927 928 case KEY_HOME: 929 handle_select(b, 0); 930 continue; 931 932 case KEY_END: 933 handle_select(b, b.entries.size() - 1); 934 continue; 935 936 case KEY_F(1): 937 if (!view_help(b)) { 938 return; 939 } 940 show_entries(b); 941 refresh(); 942 continue; 943 944 case KEY_F(5): 945 if (!handle_folder(b)) { 946 return; 947 } 948 continue; 949 950 case KEY_F(6): 951 b.sort_size = !b.sort_size; 952 if (!handle_folder(b)) { 953 return; 954 } 955 continue; 956 957 default: 958 if (isalphanum(got)) { 959 const auto i = seek_initial_next(b, got, b.selection); 960 if (i >= 0) { 961 handle_select(b, i); 962 } 963 } 964 continue; 965 } 966 } 967 } 968 969 int cat(FILE* w, const char* path) { 970 if (is_folder(path)) { 971 return NOT_A_FILE; 972 } 973 974 FILE* r = fopen(path, "rb"); 975 if (r == NULL) { 976 fprintf(stderr, ERROR_LINE("can't open file named '%s'"), path); 977 return BAD_ARGS; 978 } 979 980 unsigned char buf[IBUF_SIZE]; 981 982 while (!feof(w)) { 983 size_t len = fread(buf, sizeof(buf[0]), sizeof(buf), r); 984 if (len < 1) { 985 break; 986 } 987 988 fwrite(buf, 1, len, w); 989 } 990 991 fclose(r); 992 return 0; 993 } 994 995 // run returns the number of errors 996 int run(entry start, bool file_contents) { 997 FILE* in = fopen("/dev/tty", "rb"); 998 if (in == NULL) { 999 fprintf(stderr, ERROR_LINE("can't open tty for input")); 1000 return BAD_TTY; 1001 } 1002 1003 FILE* out = fopen("/dev/tty", "wb"); 1004 if (out == NULL) { 1005 fclose(in); 1006 fprintf(stderr, ERROR_LINE("can't open tty for output")); 1007 return BAD_TTY; 1008 } 1009 1010 // use UTF-8 1011 setlocale(LC_ALL, ""); 1012 1013 // initscr takes over stdin and stdout, which interferes with being 1014 // used/run via xargs and with emitting a chosen path at the end 1015 newterm("xterm", out, in); 1016 1017 noecho(); // don't show keys pressed 1018 keypad(stdscr, TRUE); // handle all keys correctly 1019 set_escdelay(10); // don't wait long after the escape key is pressed 1020 curs_set(0); // hide the cursor 1021 refresh(); // switch to alternate screen immediately 1022 1023 browser b; 1024 b.folder = NULL; 1025 b.width = 80; 1026 b.height = 25; 1027 b.top = 0; 1028 b.selection = 0; 1029 b.result = NULL; 1030 b.exit_code = 0; 1031 b.sort_size = false; 1032 b.help_mode = false; 1033 1034 getmaxyx(stdscr, b.height, b.width); 1035 1036 try { 1037 bf(b, start); 1038 } catch (const bad_alloc& e) { 1039 endwin(); 1040 fclose(out); 1041 fclose(in); 1042 1043 fprintf(stderr, ERROR_LINE("out of memory")); 1044 free(b.result); 1045 return BAD_ALLOC; 1046 } 1047 1048 endwin(); 1049 fclose(out); 1050 fclose(in); 1051 1052 if (b.result == NULL) { 1053 return NO_CHOICE; 1054 } 1055 1056 if (file_contents) { 1057 b.exit_code = cat(stdout, b.result); 1058 free(b.result); 1059 return b.exit_code; 1060 } 1061 1062 char buf[PATH_MAX]; 1063 const auto full = realpath(b.result, buf); 1064 fprintf(stdout, "%s\n", (full != NULL) ? full : b.result); 1065 free(b.result); 1066 return b.exit_code; 1067 } 1068 1069 int main(int argc, char** argv) { 1070 bool file_contents = false; 1071 size_t start_args = 1; 1072 1073 if (argc > 1) { 1074 if ( 1075 strcmp(argv[1], "-h") == 0 || 1076 strcmp(argv[1], "-help") == 0 || 1077 strcmp(argv[1], "--h") == 0 || 1078 strcmp(argv[1], "--help") == 0 1079 ) { 1080 for (const auto& line : help) { 1081 fprintf(stdout, "%s\n", line.c_str()); 1082 } 1083 return 0; 1084 } 1085 1086 if ( 1087 strcmp(argv[1], "-c") == 0 || 1088 strcmp(argv[1], "-f") == 0 || 1089 strcmp(argv[1], "--c") == 0 || 1090 strcmp(argv[1], "--f") == 0 1091 ) { 1092 file_contents = true; 1093 start_args++; 1094 } 1095 } 1096 1097 if (argc - start_args > 0 && strcmp(argv[1], "--") == 0) { 1098 start_args++; 1099 } 1100 1101 if (argc - start_args > 1) { 1102 const auto w = stderr; 1103 fprintf(w, ERROR_LINE("can't have multiple starting files/folders")); 1104 return BAD_ARGS; 1105 } 1106 1107 entry start; 1108 size_t size = 0; 1109 bool is_file = false; 1110 if (start_args == (size_t)argc) { 1111 start.name = const_cast<char*>("."); 1112 } else { 1113 start.name = argv[start_args]; 1114 } 1115 1116 if (!entry_info(start.name, &size, &is_file)) { 1117 const char* s = start.name; 1118 fprintf(stderr, ERROR_LINE("no file/folder named \"%s\" found"), s); 1119 return BAD_ARGS; 1120 } 1121 start.size = is_file ? ssize_t(size) : -1; 1122 1123 return run(start, file_contents) == 0 ? 0 : 1; 1124 }