forked from Bananymous/banan-os
				
			
		
			
				
	
	
		
			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;
 | |
| }
 |