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 }