/* * UNG's Not GNU * * Copyright (c) 2011-2022, Jakob Kaivo * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ #define _XOPEN_SOURCE 700 #include #include #include #include #include #include #include #include #include #include #include #include #include #include struct more_file { struct more_file *next; struct more_file *prev; FILE *f; FILE *backing; size_t topline; fpos_t *tlines; size_t nlines; size_t mark[26]; size_t nbytes; size_t *bytepos; char *buf; size_t nbuf; char *path; int eof; }; static int more_clear = 0; static int more_fastexit = 0; static int more_backspace = 1; static WINDOW *more_win = NULL; static WINDOW *more_status = NULL; enum { CTRL_B = 0x02, CTRL_D = 0x04, CTRL_F = 0x06, CTRL_G = 0x07, CTRL_L = 0x0c, CTRL_U = 0x15, }; static ssize_t more_getline(struct more_file *mf, size_t lineno) { if (mf->nlines <= lineno && mf->nlines != 0) { fsetpos(mf->backing, &(mf->tlines[mf->nlines - 1])); getline(&(mf->buf), &(mf->nbuf), mf->backing); } while (mf->nlines <= lineno) { mf->nlines++; mf->tlines = realloc(mf->tlines, mf->nlines * sizeof(*mf->tlines)); mf->bytepos = realloc(mf->bytepos, mf->nlines * sizeof(*mf->bytepos)); fgetpos(mf->f, &(mf->tlines[mf->nlines - 1])); if (getline(&(mf->buf), &(mf->nbuf), mf->f) == -1) { mf->nlines--; mf->eof = 1; return -1; } printf("%s\r", mf->buf); if (mf->nlines > 1) { mf->bytepos[mf->nlines - 1] = mf->bytepos[mf->nlines - 2] + strlen(mf->buf); } else { mf->bytepos[0] = 0; } if (mf->backing != mf->f) { fgetpos(mf->backing, &(mf->tlines[mf->nlines - 1])); fputs(mf->buf, mf->backing); } } fsetpos(mf->backing, &(mf->tlines[lineno])); return getline(&(mf->buf), &(mf->nbuf), mf->backing); } static int more_open(struct more_file *mf) { mf->f = stdin; if (mf->path && strcmp(mf->path, "-")) { mf->f = fopen(mf->path, "r"); if (!mf->f) { fprintf(stderr, "more: %s: %s\r\n", mf->path, strerror(errno)); return 1; } } fpos_t pos; if (fgetpos(mf->f, &pos) != 0) { mf->backing = tmpfile(); } else { mf->backing = mf->f; struct stat st; fstat(fileno(mf->f), &st); mf->nbytes = st.st_size; } if (mf->path == NULL) { mf->path = ""; } return 0; } static void more_close(struct more_file *mf) { /* TODO: maybe save these ? */ if (mf->backing != mf->f) { fclose(mf->backing); } if (mf->f != stdin) { fclose(mf->f); } free(mf->tlines); free(mf->buf); } static int more_printline(char *s) { int ret = -1; /* if (strlen(s) == (size_t)COLS) { s[COLS - 1] = '\0'; } */ scrollok(more_win, FALSE); for (size_t i = 0; s[i] != '\0'; i++) { int attr = 0; if (more_backspace && s[i + 1] == '\b') { if (s[i + 2] == '_') { attr = A_UNDERLINE; } else if (s[i + 2] == s[i]) { attr = A_BOLD; } else { i++; continue; } } if (attr) { wattron(more_win, attr); } waddch(more_win, s[i]); if (i % COLS == 0) { ret++; } if (attr) { wattroff(more_win, attr); i += 2; } } scrollok(more_win, TRUE); return ret; } static void more_refresh(struct more_file *mf) { int printed = 0; wmove(more_win, 0, 0); for (size_t i = mf->topline; i + printed < mf->topline + LINES; i++) { /* FIXME: account for long lines */ if (more_getline(mf, i) == -1) { break; } printed += more_printline(mf->buf); } wrefresh(more_win); } static void more_scroll(struct more_file *mf, int count, int multiple) { int by = count ? count * multiple : multiple; if (more_clear && abs(by) > LINES) { clear(); } if (by < 0) { if ((size_t)(-by) > mf->topline) { by = -mf->topline; mf->topline = 0; } else { mf->topline += by; } if (by != 0) { wscrl(more_win, by); more_refresh(mf); } return; } while (by-- > 0) { /* FIXME: account for long lines here, too */ if (more_getline(mf, mf->topline + LINES) < 0) { break; } mf->topline++; wprintw(more_win, "%s", mf->buf); } } static void mark(struct more_file *mf) { int c = wgetch(more_status); if (islower(c)) { mf->mark[c - 'a'] = mf->topline; } } static void jump(struct more_file *mf) { int c = wgetch(more_status); if (islower(c)) { mf->topline = mf->mark[c - 'a']; more_refresh(mf); } } static void more_help(void) { char path[L_tmpnam + 1]; FILE *f = fopen(tmpnam(path), "w"); if (f == NULL) { wprintw(more_status, "error creating help: %s", strerror(errno)); return; } fprintf(f, "^X means hold control and press X\n"); fprintf(f, "\n"); fprintf(f, "Non-Scrolling Commands\n"); fprintf(f, "h display this help\n"); fprintf(f, ":e filename open filename\n"); fprintf(f, "#:n go to the count-th next file\n"); fprintf(f, "#:p go to the count-th previous file\n"); fprintf(f, "v edit file with $EDITOR (default vi)\n"); fprintf(f, "= ^G display current file information\n"); fprintf(f, "q :q ZZ quit\n"); fprintf(f, "r ^L redraw the current screen\n"); fprintf(f, "R discard buffered input and redraw screen\n"); fprintf(f, "\n"); fprintf(f, "Scrolling Commands: Prefix with a count for number of lines, otherwise as shown\n"); fprintf(f, "%-20s%-30s%-10s\n", " ", "forward", "backward"); fprintf(f, "%-20s%-30s%-10s\n", "one line", "j ", "k"); fprintf(f, "%-20s%-30s%-10s\n", "one half screen", "d ^D", "u ^U"); fprintf(f, "%-20s%-30s%-10s\n", "one screen", "f ^F", "b ^B"); fprintf(f, "\n"); fprintf(f, "Jumping Commands\n"); fprintf(f, "#s skip count lines after the end of the current screen\n"); fprintf(f, "#g go to line count, or 1\n"); fprintf(f, "#G go to line count, or end of file\n"); fprintf(f, ":t tag jump to tag\n"); fprintf(f, "mletter mark the current position as letter\n"); fprintf(f, "'letter return to the position marked letter\n"); fprintf(f, "'' return to last position before large movement\n"); fprintf(f, "\n"); fprintf(f, "Searching Commands\n"); fprintf(f, "#/[!]pattern search for the count-th occurence of pattern\n"); fprintf(f, "#?[!]pattern search backward\n"); fprintf(f, "#n repeat search\n"); fprintf(f, "#N repeat search backwards\n"); fclose(f); //more(path); remove(path); } static void more_invoke_editor(struct more_file *mf) { extern char **environ; char *editor = getenv("EDITOR"); if (editor == NULL) { editor = "vi"; } char line[64]; snprintf(line, sizeof(line), "%zd", mf->topline + 1); char *argv[] = { editor, "-c", line, mf->path, NULL }; char *base = editor; char *slash = strrchr(editor, '/'); if (slash) { base = slash + 1; } if (strcmp(base, "ex") && strcmp(base, "vi")) { argv[1] = mf->path; argv[2] = NULL; } def_prog_mode(); reset_shell_mode(); pid_t pid; if (posix_spawnp(&pid, editor, NULL, NULL, argv, environ) == 0) { size_t line = mf->topline; more_close(mf); waitpid(pid, NULL, 0); more_open(mf); mf->topline = line; } else { wprintw(more_status, "%s %s: %s\n", editor, mf->path, strerror(errno)); } reset_prog_mode(); } static struct more_file more_examine(struct more_file *mf) { wprintw(more_status, "e"); char filename[1024]; wgetnstr(more_status, filename, sizeof(filename)); wclear(more_status); wordexp_t we = { 0 }; if (wordexp(filename, &we, WRDE_NOCMD) != 0) { wprintw(more_status, "couldn't expand %s", filename); return *mf; } if (we.we_wordc == 0) { return *mf; } strcpy(filename, we.we_wordv[0]); wordfree(&we); if (we.we_wordc > 1) { wprintw(more_status, "not opening multiple files"); return *mf; } if (!strcmp(filename, "#")) { strcpy(filename, mf->path); } /* struct more_file ret = more_open(filename); if (ret.f == NULL) { wprintw(more_status, "%s: %s", filename, strerror(errno)); return *mf; } more_close(mf); more_refresh(&ret); return ret; */ return *mf; } static int more(struct more_file *mf) { if (more_open(mf) != 0) { /* FIXME: try next file */ return 1; } more_refresh(mf); int count = 0; while (mf) { int at_eof = (mf->eof && (mf->topline + LINES >= mf->nlines)); wmove(more_status, 0, 0); if (at_eof) { if (more_fastexit && mf->next == NULL) { return 0; } wprintw(more_status, "%s ", mf->path); } int c = wgetch(more_status); wclear(more_status); switch (c) { case EOF: exit(2); break; case 'h': more_help(); break; case 'f': case CTRL_F: if (at_eof) { return 1; } if (count == 0) { count = LINES - 1; } more_scroll(mf, count, 1); break; case 'b': case CTRL_B: if (count == 0) { count = LINES - 1; } more_scroll(mf, count, -1); break; case ' ': count = count ? count : LINES - 1; /* FALLTHRU */ case 'j': case '\n': if (at_eof) { return 1; } more_scroll(mf, count, 1); break; case 'k': more_scroll(mf, count, -1); break; case 'd': case CTRL_D: if (at_eof) { return 1; } more_scroll(mf, count, (LINES - 1) / 2); break; case 's': if (at_eof) { return 1; } count = count ? count : 1; more_scroll(mf, LINES - 1 + count, 1); break; case 'u': case CTRL_U: more_scroll(mf, count, -(LINES - 1) / 2); break; case 'G': if (count == 0) { count = mf->nbytes; } /* FALLTHRU */ case 'g': more_scroll(mf, count - mf->topline, 1); break; case 'R': // discard(); /* FALLTHRU */ case 'r': case CTRL_L: wclear(more_win); more_refresh(mf); break; case 'm': mark(mf); break; case '\'': jump(mf); break; case '/': //search(count); break; case '?': //search(-count); break; case 'n': //repeatsearch(count); break; case 'N': //repeatsearch(-count); break; case ':': waddch(more_status, c); int c2 = wgetch(more_status); wprintw(more_status, "\b \b"); switch (c2) { case 'e': more_examine(mf); break; case 'n': if (mf->next == NULL) { /* TODO */ return 0; } more_close(mf); if (count == 0) { count = 1; } while (count-- > 0 && mf->next) { mf = mf->next; } wprintw(more_status, "%s", mf->path); more_open(mf); clear(); more_refresh(mf); break; case 'p': if (mf->prev == NULL) { break; } more_close(mf); if (count == 0) { count = 1; } while (count-- > 0 && mf->prev) { mf = mf->prev; } more_open(mf); clear(); more_refresh(mf); break; case 't': // tagstring(); break; case 'q': exit(0); default: break; } break; case 'v': more_invoke_editor(mf); break; case '=': case CTRL_G: wprintw(more_status, "%s; File %d/%d; Line %zd; Byte %zd/%zd; %zd%%", mf->path, 0, 0, mf->topline, mf->bytepos[mf->topline], mf->nbytes, 100 * mf->bytepos[mf->topline] / mf->nbytes); break; case 'Z': if (wgetch(more_status) != 'Z') { break; } /* FALLTHRU */ case 'q': exit(0); case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': count = count * 10 + (c - '0'); break; default: break; } if (!isdigit(c)) { count = 0; } wrefresh(more_win); } return 0; } static void adjust_args(int *argc, char ***argv) { char *env = getenv("MORE"); if (env) { char **newargv = malloc((*argc + 2) * sizeof(*newargv)); newargv[0] = (*argv)[0]; /* TODO: account for spaces in env */ newargv[1] = env; for (int i = 1; i < *argc; i++) { newargv[i + 1] = (*argv)[i]; } newargv[*argc + 1] = NULL; *argv = newargv; (*argc)++; } for (int i = 1; i < *argc; i++) { if (!strcmp((*argv)[i], "--")) { return; } if ((*argv)[i][0] == '+') { fprintf(stderr, "more: adjusting command line option +%s to -%s\n", (*argv)[i] + 1, (*argv)[i] + 1); (*argv)[i][0] = '-'; } } } static void cat_loop(FILE *f) { int c = 0; while ((c = fgetc(f)) != EOF) { putchar(c); } } static void compress_loop(FILE *f) { static int nl = 0; int c = 0; while ((c = fgetc(f)) != EOF) { if (c == '\n') { if (nl == 2) { continue; } else if (nl == 1) { nl = 2; } else { nl = 1; } } else { nl = 0; } putchar(c); } } static int more_cat(const char *path, void (*loop)(FILE *)) { FILE *f = stdin; if (path != NULL && strcmp(path, "-") != 0) { f = fopen(path, "r"); if (!f) { fprintf(stderr, "more: %s: %s\n", path, strerror(errno)); return 1; } } loop(f); if (f != stdin) { fclose(f); } return 0; } static void more_exit(void) { endwin(); } int main(int argc, char *argv[]) { setlocale(LC_ALL, ""); int ignorecase = 0; int compressempty = 0; adjust_args(&argc, &argv); int c; while ((c = getopt(argc, argv, "ceisun:p:t:")) != -1) { switch (c) { case 'c': more_clear = 1; break; case 'e': more_fastexit = 1; break; case 'i': ignorecase = 1; break; case 's': compressempty = 1; break; case 'u': more_backspace = 0; break; case 'n': setenv("LINES", optarg, 1); break; case 'p': //perfile = optarg; break; case 't': //tag = optarg; break; default: return 1; } } if (!isatty(STDOUT_FILENO)) { int ret = 0; void (*loop)(FILE*) = compressempty ? compress_loop : cat_loop; do { ret |= more_cat(argv[optind++], loop); } while (optind < argc); return ret; } if (newterm(NULL, stdout, stderr) == NULL) { FILE *tty = fopen("/dev/tty", "r"); if (tty == NULL) { perror("more: couldn't open terminal"); return 1; } if (newterm(NULL, stdout, tty) == NULL) { perror("more: couldn't open terminal"); return 1; } } cbreak(); atexit(more_exit); more_win = newwin(LINES - 1, 0, 0, 0); scrollok(more_win, TRUE); more_status = newwin(1, 0, LINES - 1, 0); touchwin(stdscr); if (more_clear) { clear(); } struct more_file *head = NULL; do { struct more_file *mf = calloc(1, sizeof(*mf)); if (!mf) { perror("more"); return 1; } mf->path = argv[optind]; if (head == NULL) { head = mf; } else { struct more_file *c = head; while (c->next != NULL) { c = c->next; } c->next = mf; mf->prev = c; } } while (argv[optind++]); (void)ignorecase; return more(head); }