#include <getopt.h>
#include <error.h>
#include <string.h>
#include <ctype.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <termios.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <errno.h>
#include <stdarg.h>
#define CS_REVERSE "\e[7m"
#define CS_NORMAL "\e[0m"
#define CS_ERASE_BEGLINE "\e[1K"
#define CS_ERASE_SCREEN "\e[2J"
#define CS_HOME "\e[H"
#define LINE_BUF_SIZE 256
#define ctrl(letter) ((letter) & 077)
#define min(a, b) ({ \
typeof (a) _a = (a); \
typeof (b) _b = (b); \
_a < _b ? _a : _b; \
})
enum {
CM_NONE,
CM_NEXT_LINE,
CM_NEXT_SCREENFUL,
CM_SCROLL,
CM_SKIP_BW_LINE,
CM_SKIP_BW_SCREENFUL,
CM_SKIP_FW_LINE,
CM_SKIP_FW_SCREENFUL,
CM_BEGIN,
CM_END,
CM_REDRAW,
CM_DISPLAY_LINE_NR,
CM_DISPLAY_FILENAME,
CM_PREVIOUS_FILE,
CM_NEXT_FILE,
CM_EXIT,
CM_HELP,
CM_REPEAT
};
enum {BT_NONE, BT_BANNER, BT_PAUSE};
static const char textcntrl[] = {'\b', '\t', '\n', '\f', '\r'};
#define isprint_ext(c) (isprint(c) || (c) >= 160)
#define istext(c) (isprint_ext(c) || memchr(textcntrl, (c), sizeof textcntrl))
static int has_screen;
static struct winsize winsize;
static int screen_lines;
static int scroll_lines;
static struct termios saved_termios;
static int last_cmd;
static inline void ring_bell()
{
fputc('\a', stderr);
}
static void setup_term()
{
struct termios new_termios;
tcgetattr(fileno(stderr), &saved_termios);
new_termios = saved_termios;
new_termios.c_lflag &= ~(ICANON | ECHO | ISIG);
new_termios.c_cc[VMIN] = 1;
new_termios.c_cc[VTIME] = 0;
tcsetattr(fileno(stderr), TCSANOW, &new_termios);
}
static void restore_term()
{
tcsetattr(fileno(stderr), TCSANOW, &saved_termios);
}
static int read_char()
{
unsigned char c;
if (read(fileno(stderr), &c, 1) == -1) {
restore_term();
error(EXIT_FAILURE, errno, "read error");
}
return c;
}
static int read_cmd()
{
int state;
int c;
state = 0;
while (1) {
c = read_char();
switch (state) {
case 0:
switch (c) {
case 'h':
case '?':
return CM_HELP;
case ' ':
return CM_NEXT_SCREENFUL;
case 'z':
return CM_NEXT_SCREENFUL;
case '\n':
return CM_NEXT_LINE;
case 'd':
case ctrl('D'):
return CM_SCROLL;
case 'q':
case 'Q':
case ctrl('C'):
return CM_EXIT;
case 's':
return CM_SKIP_FW_LINE;
case 'f':
return CM_SKIP_FW_SCREENFUL;
case 'b':
case ctrl('B'):
return CM_SKIP_BW_SCREENFUL;
case '=':
return CM_DISPLAY_LINE_NR;
case ctrl('L'):
return CM_REDRAW;
case '.':
return CM_REPEAT;
case ':':
state = 1;
continue;
case '\e':
state = 2;
continue;
}
break;
case 1:
switch (c) {
case 'n':
return CM_NEXT_FILE;
case 'p':
return CM_PREVIOUS_FILE;
case 'f':
return CM_DISPLAY_FILENAME;
}
break;
case 2:
if (c == '[') {
state = 3;
continue;
}
break;
case 3:
switch (c) {
case 'A':
return CM_SKIP_BW_LINE;
case 'B':
return CM_NEXT_LINE;
case 'C':
return CM_NEXT_FILE;
case 'D':
return CM_PREVIOUS_FILE;
case '1':
state = 4;
continue;
case '4':
state = 5;
continue;
case '5':
state = 6;
continue;
case '6':
state = 7;
continue;
}
break;
case 4:
if (c == '~')
return CM_BEGIN;
break;
case 5:
if (c == '~')
return CM_END;
break;
case 6:
if (c == '~')
return CM_SKIP_BW_SCREENFUL;
break;
case 7:
if (c == '~')
return CM_NEXT_SCREENFUL;
break;
}
ring_bell();
state = 0;
}
}
static void display_prompt(const char *format, va_list ap)
{
fputs(CS_REVERSE, stdout);
fputs("--More--", stdout);
if (format) {
putchar('(');
vprintf(format, ap);
putchar(')');
}
fputs(CS_NORMAL, stdout);
fflush(stdout);
}
static void display_line(int line)
{
printf("%d", line);
fflush(stdout);
}
static void display_filename(const char *filename, int line)
{
if (filename)
printf("\"%s\"", filename);
else
printf("[Not a file]");
printf(" line %d", line);
fflush(stdout);
}
static void erase_prompt()
{
fputs(CS_ERASE_BEGLINE, stdout);
putchar('\r');
}
static int pause_format(const char *format, ...)
{
va_list ap;
int cmd;
va_start(ap, format);
display_prompt(format, ap);
va_end(ap);
cmd = read_cmd();
erase_prompt();
return cmd;
}
static int pause_void()
{
return pause_format(NULL);
}
static int pause_percent(int percent)
{
return pause_format("%d%%", percent);
}
static int pause_nextfile(const char *filename)
{
return pause_format("Next file: %s", filename);
}
static int pause_line(int line)
{
int cmd;
display_line(line);
cmd = read_cmd();
erase_prompt();
return cmd;
}
static int pause_filename(const char *filename, int line)
{
int cmd;
display_filename(filename, line);
cmd = read_cmd();
erase_prompt();
return cmd;
}
static int must_clear_screen(FILE *fp)
{
int c;
if ((c = getc(fp)) != EOF)
ungetc(c, fp);
return c == '\f';
}
static int get_line(FILE *fp, char buf[], size_t *len)
{
int col;
int formfeed;
int need_wrap;
char *p;
int c;
col = 0;
formfeed = 0;
need_wrap = 0;
p = buf;
while (p != &buf[LINE_BUF_SIZE - 1]) {
if ((c = getc(fp)) == EOF) {
if (p > buf)
goto end_line;
else
goto end;
}
*p++ = c;
switch (c) {
case '\b':
if (col > 0) {
col--;
need_wrap = 0;
}
continue;
case '\t':
col = min((col | 7) + 1, winsize.ws_col - 1);
continue;
case '\n':
goto end;
case '\f':
if (need_wrap)
goto wrap;
p[-1] = '^';
*p++ = 'L';
formfeed = 1;
if (col + 2 < winsize.ws_col)
col += 2;
else
goto end;
continue;
case '\r':
col = 0;
need_wrap = 0;
continue;
}
if (isprint_ext(c)) {
if (need_wrap)
goto wrap;
if (col + 1 < winsize.ws_col)
col++;
else
need_wrap = 1;
}
continue;
wrap:
ungetc(c, fp);
p[-1] = '\n';
goto end;
}
end_line:
*p++ = '\n';
end:
*len = p - buf;
return formfeed;
}
static int more_chars(FILE *fp)
{
int c;
if ((c = getc(fp)) == EOF)
return 0;
ungetc(c, fp);
return 1;
}
static void clear_screen()
{
fputs(CS_ERASE_SCREEN, stdout);
fputs(CS_HOME, stdout);
}
static void print_separator()
{
puts("-------------------------------------------------------------------------------");
}
static void print_skipping_lines(int lines)
{
putchar('\n');
if (lines == 1)
puts("...skipping one line");
else
printf("...skipping %d lines\n", lines);
}
static void print_skipping_back_lines(int lines)
{
putchar('\n');
if (lines == 1)
puts("...skipping back one line");
else
printf("...skipping back %d lines\n", lines);
}
static void print_skipping_back_pages(int pages)
{
putchar('\n');
if (pages == 1)
puts("...back 1 page");
else
printf("...back %d pages\n", pages);
}
static void print_skipping_back_to_beginning()
{
putchar('\n');
puts("...skipping back to beginning");
}
static void print_skipping_to_file(const char *filename)
{
putchar('\n');
puts("...Skipping");
printf("...Skipping to file %s\n", filename);
putchar('\n');
}
static void print_skipping_back_to_file(const char *filename)
{
putchar('\n');
puts("...Skipping");
printf("...Skipping back to file %s\n", filename);
putchar('\n');
}
static void print_banner(const char *filename)
{
puts("::::::::::::::");
puts(filename);
puts("::::::::::::::");
}
static void print_directory(const char *filename)
{
putchar('\n');
printf("*** %s: directory ***\n", filename);
putchar('\n');
}
static void print_nottextfile(const char *filename)
{
putchar('\n');
printf("******** %s: Not a text file ********\n", filename);
putchar('\n');
}
static void print_file(FILE *fp)
{
int c;
while ((c = getc(fp)) != EOF)
putchar(c);
}
static void skip_lines(FILE *fp, int lines)
{
char buf[LINE_BUF_SIZE + 1];
size_t len;
while (lines > 0) {
get_line(fp, buf, &len);
if (!len)
break;
lines--;
}
}
static void print_line(const char *s, size_t len)
{
while (len--)
putchar(*s++);
}
static int print_lines(FILE *fp, int lines)
{
int n;
char buf[LINE_BUF_SIZE + 1];
size_t len;
int need_pause;
if (lines >= 0) {
n = 0;
while (n < lines) {
need_pause = get_line(fp, buf, &len);
if (!len)
return -1;
print_line(buf, len);
if (!more_chars(fp))
return -1;
n++;
if (need_pause)
break;
}
return n;
}
else
while (1) {
get_line(fp, buf, &len);
if (!len)
return -1;
print_line(buf, len);
}
}
static void print_commands()
{
puts(" h or ? Help: display a summary of these commands");
puts(" <space> Display next screenful of text");
puts(" z Display next screenful of text");
puts(" <return> Display next line of text");
puts(" d or ctrl-D Scroll 1 half-screenful of text");
puts(" q or Q Exit from more");
puts(" s Skip forward 1 line of text");
puts(" f Skip forward 1 screenful of text");
puts(" b or ctrl-B Skip backwards 1 screenful of text");
puts(" = Display current line number");
puts(" ctrl-L Redraw screen");
puts(" :n Go to next file");
puts(" :p Go to previous file");
puts(" :f Display current file name and line number");
puts(" . Repeat previous command");
puts(" <up> Skip backward 1 line of text");
puts(" <down> Display next line of text");
puts(" <right> Go to next file");
puts(" <left> Go to previous file");
puts(" <pgup> Skip backwards 1 screenful of text");
puts(" <pgdn> Display next screenful of text");
puts(" <home> Go to beginning of current file");
puts(" <end> Go to end of current file");
}
static inline int get_percent(FILE *fp, const struct stat *statbuf)
{
long offset;
if (statbuf->st_size == 0 || (offset = ftell(fp)) == -1)
return 0;
return offset * 100 / statbuf->st_size;
}
static int more(const char *filename, int banner_type, int *next_file)
{
int err;
struct stat statbuf;
int seekable;
FILE *fp;
unsigned char buf[1];
int line;
int started;
int cmd;
int lines;
err = 0;
*next_file = 0;
if (filename) {
if (stat(filename, &statbuf) == -1) {
error(0, errno, filename);
err = 1;
goto end;
}
if (S_ISDIR(statbuf.st_mode)) {
print_directory(filename);
goto end;
}
seekable = S_ISREG(statbuf.st_mode);
if (!(fp = fopen(filename, "r"))) {
error(0, errno, filename);
err = 1;
goto end;
}
if (fread(buf, 1, sizeof buf, fp) == sizeof buf) {
if (!istext(buf[0])) {
print_nottextfile(filename);
goto fclose;
}
rewind(fp);
}
}
else {
if (fstat(fileno(stdin), &statbuf) == -1) {
error(0, errno, "standard input");
err = 1;
goto end;
}
seekable = 0;
fp = stdin;
}
if (has_screen) {
line = 0;
started = 0;
if (banner_type == BT_PAUSE)
goto pause;
lines = screen_lines;
while (1) {
if (!started) {
if (must_clear_screen(fp))
clear_screen();
if (banner_type != BT_NONE) {
print_banner(filename);
if (screen_lines < 3)
lines = 0;
else if (lines > screen_lines - 3)
lines = screen_lines - 3;
}
started = 1;
}
if ((lines = print_lines(fp, lines)) == -1)
break;
line += lines;
pause:
if (!started)
cmd = pause_nextfile(filename);
else if (seekable)
cmd = pause_percent(get_percent(fp, &statbuf));
else
cmd = pause_void();
process_cmd:
if (cmd == CM_REPEAT) {
if (last_cmd == CM_NONE)
goto invalid_cmd;
cmd = last_cmd;
}
else
last_cmd = cmd;
switch (cmd) {
case CM_NEXT_LINE:
lines = 1;
continue;
case CM_NEXT_SCREENFUL:
lines = screen_lines;
continue;
case CM_SCROLL:
lines = scroll_lines;
continue;
case CM_SKIP_BW_LINE:
if (!seekable)
goto invalid_cmd;
print_skipping_back_lines(1);
fseek(fp, 0, SEEK_SET);
if ((line -= screen_lines + 1) < 0)
line = 0;
skip_lines(fp, line);
lines = screen_lines;
continue;
case CM_SKIP_BW_SCREENFUL:
if (!seekable)
goto invalid_cmd;
print_skipping_back_pages(1);
fseek(fp, 0, SEEK_SET);
if ((line -= 2 * screen_lines) < 0)
line = 0;
skip_lines(fp, line);
lines = screen_lines;
continue;
case CM_SKIP_FW_LINE:
print_skipping_lines(1);
skip_lines(fp, 1);
line++;
lines = screen_lines;
continue;
case CM_SKIP_FW_SCREENFUL:
print_skipping_lines(screen_lines);
skip_lines(fp, screen_lines);
line += screen_lines;
lines = screen_lines;
continue;
case CM_BEGIN:
if (!seekable)
goto invalid_cmd;
print_skipping_back_to_beginning();
fseek(fp, 0, SEEK_SET);
line = 0;
lines = screen_lines;
continue;
case CM_END:
lines = -1;
continue;
case CM_REDRAW:
if (!seekable)
goto invalid_cmd;
clear_screen();
fseek(fp, 0, SEEK_SET);
if ((line -= screen_lines) < 0)
line = 0;
skip_lines(fp, line);
lines = screen_lines;
continue;
case CM_DISPLAY_LINE_NR:
cmd = pause_line(line);
goto process_cmd;
case CM_DISPLAY_FILENAME:
cmd = pause_filename(filename, line);
goto process_cmd;
case CM_NEXT_FILE:
*next_file = 1;
goto end_file;
case CM_PREVIOUS_FILE:
if (!filename)
goto invalid_cmd;
*next_file = -1;
goto end_file;
case CM_EXIT:
restore_term();
exit(EXIT_SUCCESS);
case CM_HELP:
print_separator();
print_commands();
print_separator();
goto pause;
}
invalid_cmd:
ring_bell();
goto pause;
}
}
else {
if (banner_type != BT_NONE)
print_banner(filename);
print_file(fp);
}
end_file:
if (filename) {
fclose:
fflush(stdout);
if (fclose(fp) == EOF) {
error(0, errno, filename);
err = 1;
goto end;
}
}
end:
return err ? -1 : 0;
}
static void __attribute__ ((noreturn)) die_bad_usage()
{
fprintf(stderr, "Try `%s --help' for more information.\n", program_invocation_name);
exit(EXIT_FAILURE);
}
static void print_help()
{
printf("Usage: %s [OPTION]... [FILE]...\n", program_invocation_name);
puts("Print file(s) to standard output, screen by screen.");
putchar('\n');
puts("Options are:");
puts(" -h, --help display this help and exit");
putchar('\n');
puts("Commands are:");
print_commands();
}
int main(int argc, char **argv)
{
static const struct option longopts[] = {
{"help", no_argument, NULL, 'h'},
{NULL, 0, NULL, 0}
};
int err;
char **filename_list;
int filename_count;
int filename_ind;
int next_file;
int banner_type;
int c;
while ((c = getopt_long(argc, argv, "h", longopts, NULL)) != -1)
switch (c) {
case 'h':
print_help();
return EXIT_SUCCESS;
case '?':
die_bad_usage();
}
has_screen = ioctl(fileno(stdout), TIOCGWINSZ, &winsize) == 0 && winsize.ws_row > 1 && winsize.ws_col > 0;
if (has_screen) {
screen_lines = winsize.ws_row - 1;
scroll_lines = winsize.ws_row / 2 - 1 ? : 1;
setup_term();
}
err = 0;
filename_list = argv + optind;
filename_count = argc - optind;
last_cmd = CM_NONE;
if (!isatty(fileno(stdin))) {
if (filename_count > 0 && !strcmp(filename_list[0], "-")) {
filename_list++;
filename_count--;
}
if (more(NULL, BT_NONE, &next_file) == -1) {
err = 1;
next_file = 0;
}
filename_ind = (next_file ? : 1) - 1;
banner_type = BT_PAUSE;
}
else if (filename_count > 0) {
filename_ind = 0;
next_file = 0;
banner_type = filename_count > 1 ? BT_BANNER : BT_NONE;
}
else {
if (has_screen)
restore_term();
error(0, 0, "missing operand");
die_bad_usage();
}
while (1) {
if (filename_ind < 0)
filename_ind = 0;
if (filename_ind >= filename_count)
break;
if (next_file < 0)
print_skipping_back_to_file(filename_list[filename_ind]);
else if (next_file > 0)
print_skipping_to_file(filename_list[filename_ind]);
if (more(filename_list[filename_ind], banner_type, &next_file) == -1)
err = 1;
banner_type = BT_PAUSE;
filename_ind += next_file ? : 1;
}
if (has_screen)
restore_term();
return err ? EXIT_FAILURE : EXIT_SUCCESS;
}