Userspace can freely set terminal size, kernel just updates it when for example new font is loaded. Also SIGWINCH is now sent by kernel instead of userspace.
368 lines
7.1 KiB
C++
368 lines
7.1 KiB
C++
#include <BAN/Vector.h>
|
|
#include <BAN/String.h>
|
|
|
|
#include <ctype.h>
|
|
#include <fcntl.h>
|
|
#include <sys/ioctl.h>
|
|
#include <sys/select.h>
|
|
#include <termios.h>
|
|
#include <unistd.h>
|
|
|
|
struct config_t
|
|
{
|
|
bool raw { false };
|
|
bool quit_if_one_screen { false };
|
|
bool no_init { false };
|
|
};
|
|
static config_t config;
|
|
|
|
static void usage(const char* argv0, int ret)
|
|
{
|
|
FILE* fout = ret ? stderr : stdout;
|
|
fprintf(fout, "usage: %s [OPTIONS]... [--] [FILE]...\n", argv0);
|
|
fprintf(fout, " -r, -R, --raw print ANSI control sequences\n");
|
|
fprintf(fout, " -F, -quit-if-one-screen exit immediately if output fits in one screen\n");
|
|
fprintf(fout, " -X, -no-init don't clear screen at the startup\n");
|
|
fprintf(fout, " -h, --help show this message and exit\n");
|
|
exit(ret);
|
|
}
|
|
|
|
static void handle_option_short(const char* argv0, char opt, bool exit_on_error)
|
|
{
|
|
switch (opt)
|
|
{
|
|
case 'r':
|
|
case 'R':
|
|
config.raw = true;
|
|
break;
|
|
case 'F':
|
|
config.quit_if_one_screen = true;
|
|
break;
|
|
case 'X':
|
|
config.no_init = true;
|
|
break;
|
|
case 'h':
|
|
usage(argv0, 0);
|
|
break;
|
|
default:
|
|
fprintf(stderr, "unknown option: %c\n", opt);
|
|
fprintf(stderr, "see --help for usage\n");
|
|
if (exit_on_error)
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
static void handle_option_long(const char* argv0, const char* opt, bool exit_on_error)
|
|
{
|
|
if (!strcmp(opt, "--raw"))
|
|
config.raw = true;
|
|
else if (!strcmp(opt, "--quit-if-one-screen"))
|
|
config.quit_if_one_screen = true;
|
|
else if (!strcmp(opt, "--no-init"))
|
|
config.no_init = true;
|
|
else if (!strcmp(opt, "--help"))
|
|
usage(argv0, 0);
|
|
else
|
|
{
|
|
fprintf(stderr, "unknown option: %s\n", opt);
|
|
fprintf(stderr, "see --help for usage\n");
|
|
if (exit_on_error)
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
static int parse_config_or_exit(int argc, char** argv)
|
|
{
|
|
// FIXME: long environment options
|
|
if (const char* env = getenv("LESS"))
|
|
for (size_t i = 0; env[i]; i++)
|
|
(void)handle_option_short(argv[0], env[i], false);
|
|
|
|
int i = 1;
|
|
for (; i < argc; i++)
|
|
{
|
|
if (argv[i][0] != '-' || !strcmp(argv[i], "--"))
|
|
break;
|
|
if (argv[i][1] == '-')
|
|
handle_option_long(argv[0], argv[i], true);
|
|
else for (size_t j = 1; argv[i][j]; j++)
|
|
handle_option_short(argv[0], argv[i][j], true);
|
|
}
|
|
return i;
|
|
}
|
|
|
|
static int get_keyboard_fd()
|
|
{
|
|
// if stdin is a terminal, use it
|
|
if (isatty(STDIN_FILENO))
|
|
return STDIN_FILENO;
|
|
|
|
// otherwise try to open our controlling terminal
|
|
int fd = open("/dev/tty", O_RDONLY);
|
|
if (fd != -1)
|
|
return fd;
|
|
|
|
// if that fails, try stderr as the last fallback
|
|
if (isatty(STDERR_FILENO))
|
|
return STDERR_FILENO;
|
|
|
|
return -1;
|
|
}
|
|
|
|
static int output_to_non_terminal(int fd)
|
|
{
|
|
for (;;)
|
|
{
|
|
char buffer[128];
|
|
const ssize_t nread = read(fd, buffer, sizeof(buffer));
|
|
if (nread == -1)
|
|
perror("read");
|
|
if (nread <= 0)
|
|
break;
|
|
|
|
ssize_t total = 0;
|
|
while (total < nread)
|
|
{
|
|
const ssize_t nwrite = write(STDOUT_FILENO, buffer + total, nread - total);
|
|
if (nwrite < 0)
|
|
perror("write");
|
|
if (nwrite <= 0)
|
|
break;
|
|
total += nwrite;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static bool read_lines(int fd, winsize ws, BAN::Vector<BAN::String>& out)
|
|
{
|
|
char buffer[128];
|
|
const ssize_t nread = read(fd, buffer, sizeof(buffer));
|
|
if (nread < 0)
|
|
perror("read");
|
|
if (nread <= 0)
|
|
return false;
|
|
|
|
if (out.empty())
|
|
MUST(out.emplace_back());
|
|
|
|
bool in_ansi = false;
|
|
|
|
size_t col = 0;
|
|
for (size_t i = 0; i < out.back().size(); i++)
|
|
{
|
|
if (in_ansi)
|
|
{
|
|
if (isalpha(out.back()[i]))
|
|
in_ansi = false;
|
|
}
|
|
else if (out.back()[i] == '\e')
|
|
in_ansi = true;
|
|
else
|
|
col++;
|
|
}
|
|
|
|
for (ssize_t i = 0; i < nread; i++)
|
|
{
|
|
if (in_ansi)
|
|
{
|
|
if (config.raw)
|
|
MUST(out.back().push_back(buffer[i]));
|
|
if (isalpha(buffer[i]))
|
|
in_ansi = false;
|
|
continue;
|
|
}
|
|
|
|
const auto append_char =
|
|
[&col, &out, ws](char ch)
|
|
{
|
|
if (col >= ws.ws_col)
|
|
{
|
|
MUST(out.emplace_back());
|
|
col = 0;
|
|
}
|
|
MUST(out.back().push_back(ch));
|
|
col++;
|
|
};
|
|
|
|
switch (buffer[i])
|
|
{
|
|
case '\e':
|
|
if (config.raw)
|
|
MUST(out.back().push_back(buffer[i]));
|
|
in_ansi = true;
|
|
break;
|
|
case '\n':
|
|
MUST(out.emplace_back());
|
|
col = 0;
|
|
break;
|
|
case '\t':
|
|
append_char(' ');
|
|
while (col % 8)
|
|
append_char(' ');
|
|
break;
|
|
default:
|
|
append_char(buffer[i]);
|
|
break;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool less_file(int fd, int kb_fd, winsize ws)
|
|
{
|
|
if (!isatty(STDOUT_FILENO) || kb_fd == -1 || ws.ws_col == 0 || ws.ws_row == 0)
|
|
return output_to_non_terminal(fd);
|
|
|
|
BAN::Vector<BAN::String> lines;
|
|
size_t lines_size = 0;
|
|
const auto update_lines_size =
|
|
[&lines, &lines_size]
|
|
{
|
|
lines_size = lines.size();
|
|
if (!lines.empty() && lines.back().empty())
|
|
lines_size--;
|
|
};
|
|
|
|
while (lines_size < ws.ws_row)
|
|
{
|
|
if (!read_lines(fd, ws, lines))
|
|
break;
|
|
update_lines_size();
|
|
}
|
|
|
|
if (!config.no_init)
|
|
{
|
|
int y = 1;
|
|
if (lines_size < ws.ws_row)
|
|
y = ws.ws_row - lines_size;
|
|
printf("\e[2J\e[%dH", y);
|
|
}
|
|
|
|
for (size_t i = 0; i < lines_size && i < static_cast<size_t>(ws.ws_row - 1); i++)
|
|
printf("%s\n", lines[i].data());
|
|
fflush(stdout);
|
|
|
|
if (lines_size < ws.ws_row && config.quit_if_one_screen)
|
|
return true;
|
|
|
|
printf(":");
|
|
fflush(stdout);
|
|
|
|
size_t line = 0;
|
|
for (;;)
|
|
{
|
|
char ch;
|
|
if (read(kb_fd, &ch, 1) != 1)
|
|
break;
|
|
switch (ch)
|
|
{
|
|
case 'q':
|
|
printf("\e[G\e[K");
|
|
fflush(stdout);
|
|
return true;
|
|
case '\e':
|
|
{
|
|
if (read(kb_fd, &ch, 1) != 1 || ch != '[')
|
|
break;
|
|
if (read(kb_fd, &ch, 1) != 1)
|
|
break;
|
|
switch (ch)
|
|
{
|
|
case 'A':
|
|
if (line == 0)
|
|
break;
|
|
line--;
|
|
|
|
printf("\e[H");
|
|
for (int i = 0; i < ws.ws_row - 1; i++)
|
|
printf("%s\e[K\n", lines[line + i].data());
|
|
printf(":\e[K");
|
|
fflush(stdout);
|
|
|
|
break;
|
|
case 'B':
|
|
while (lines_size - (line + 1) < ws.ws_row)
|
|
{
|
|
if (!read_lines(fd, ws, lines))
|
|
break;
|
|
update_lines_size();
|
|
}
|
|
if (lines_size - (line + 1) < static_cast<size_t>(ws.ws_row - 1))
|
|
break;
|
|
line++;
|
|
|
|
printf("\e[H");
|
|
for (int i = 0; i < ws.ws_row - 1; i++)
|
|
printf("%s\e[K\n", lines[line + i].data());
|
|
printf(":\e[K");
|
|
fflush(stdout);
|
|
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
int main(int argc, char** argv)
|
|
{
|
|
int i = parse_config_or_exit(argc, argv);
|
|
|
|
int kb_fd = get_keyboard_fd();
|
|
if (kb_fd != -1)
|
|
{
|
|
int flags = 0;
|
|
fcntl(kb_fd, F_GETFL, &flags);
|
|
if (flags & O_NONBLOCK)
|
|
fcntl(kb_fd, F_SETFL, flags & ~O_NONBLOCK);
|
|
|
|
termios termios;
|
|
tcgetattr(kb_fd, &termios);
|
|
termios.c_lflag &= ~(ECHO | ICANON);
|
|
tcsetattr(kb_fd, TCSANOW, &termios);
|
|
}
|
|
|
|
winsize ws {};
|
|
if (isatty(STDOUT_FILENO))
|
|
{
|
|
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0)
|
|
{
|
|
// try to make stdout fully buffered to reduce flicker
|
|
const size_t bufsize = 2 * ws.ws_row * ws.ws_col;
|
|
if (char* buffer = static_cast<char*>(malloc(bufsize)))
|
|
setvbuf(stdout, buffer, _IOFBF, bufsize);
|
|
}
|
|
}
|
|
|
|
if (i == argc)
|
|
{
|
|
if (!less_file(STDIN_FILENO, kb_fd, ws))
|
|
return EXIT_FAILURE;
|
|
return EXIT_SUCCESS;
|
|
}
|
|
|
|
int ret = EXIT_SUCCESS;
|
|
for (; i < argc; i++)
|
|
{
|
|
int fd = open(argv[i], O_RDONLY);
|
|
if (fd == -1)
|
|
{
|
|
perror(argv[i]);
|
|
continue;
|
|
}
|
|
|
|
if (!less_file(fd, kb_fd, ws))
|
|
ret = EXIT_FAILURE;
|
|
|
|
close(fd);
|
|
}
|
|
|
|
return ret;
|
|
}
|