diff pager.c @ 17:a08ef0674d8e

Page long output in interactive mode
author Guido Berhoerster <guido+pwm@berhoerster.name>
date Sat, 12 Aug 2017 10:41:52 +0200
parents
children 1e39a251cbe9
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pager.c	Sat Aug 12 10:41:52 2017 +0200
@@ -0,0 +1,400 @@
+/*
+ * Copyright (C) 2017 Guido Berhoerster <guido+pwm@berhoerster.name>
+ *
+ * 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.
+ */
+
+#include "compat.h"
+
+#ifdef	HAVE_ERR_H
+#include <err.h>
+#endif /* HAVE_ERR_H */
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <signal.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#ifdef	HAVE_SYS_QUEUE_H
+#include <sys/queue.h>
+#endif /* HAVE_SYS_QUEUE_H */
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <termios.h>
+#include <unistd.h>
+#include <wchar.h>
+
+#include "pwm.h"
+#include "pager.h"
+#include "util.h"
+
+#ifndef	TCSASOFT
+#define	TCSASOFT 0
+#endif /* !TCSASOFT */
+
+#ifndef	_NSIG
+#ifdef NSIG
+#define	_NSIG NSIG
+#else /* NSIG */
+#define	_NSIG 128
+#endif /* NSIG */
+#endif /* !_NSIG */
+
+#ifndef	_PATH_TTY
+#define	_PATH_TTY	"/dev/tty"
+#endif
+
+struct pager {
+	TAILQ_HEAD(lines_head, lines_entry) lines_head;
+	FILE		*fp;
+	size_t		buf_size;
+	char		*buf;
+};
+
+struct lines_entry {
+	TAILQ_ENTRY(lines_entry) entry;
+	char		*line;
+};
+
+static volatile sig_atomic_t signals[_NSIG];
+
+static void
+sig_handler(int signo)
+{
+	signals[signo] = 1;
+}
+
+static int
+getch_prompt(const char *prompt)
+{
+	int		retval = -1;
+	int		signo;
+	int		fd;
+	struct termios	saved_term;
+	struct termios	term;
+	struct sigaction sa;
+	struct sigaction saved_sa_hup;
+	struct sigaction saved_sa_int;
+	struct sigaction saved_sa_quit;
+	struct sigaction saved_sa_pipe;
+	struct sigaction saved_sa_alrm;
+	struct sigaction saved_sa_term;
+	struct sigaction saved_sa_tstp;
+	struct sigaction saved_sa_ttin;
+	struct sigaction saved_sa_ttou;
+	char		c;
+	int		need_restart = 0;
+
+	printf("%s", prompt);
+	fflush(stdout);
+
+restart:
+	for (signo = 0; signo < _NSIG; signo++) {
+		signals[signo] = 0;
+	}
+
+	fd = open(_PATH_TTY, O_RDONLY);
+	if (fd < 0) {
+		return (-1);
+	}
+
+	if (tcgetattr(fd, &saved_term) != 0) {
+		close(fd);
+		return (-1);
+	}
+	memcpy(&term, &saved_term, sizeof (struct termios));
+
+	/* enter raw mode and turn echo off */
+	term.c_lflag &= ~(ICANON | ECHO);
+	while ((tcsetattr(fd, TCSAFLUSH | TCSASOFT, &term) < 0) &&
+	    (errno != EINTR)) {
+		continue;
+	}
+
+	/* install temporary signal handler */
+	sigemptyset(&sa.sa_mask);
+	sa.sa_flags = 0;
+	sa.sa_handler = sig_handler;
+	sigaction(SIGHUP, &sa, &saved_sa_hup);
+	sigaction(SIGINT, &sa, &saved_sa_int);
+	sigaction(SIGQUIT, &sa, &saved_sa_quit);
+	sigaction(SIGPIPE, &sa, &saved_sa_pipe);
+	sigaction(SIGALRM, &sa, &saved_sa_alrm);
+	sigaction(SIGTERM, &sa, &saved_sa_term);
+	sigaction(SIGTSTP, &sa, &saved_sa_tstp);
+	sigaction(SIGTTIN, &sa, &saved_sa_ttin);
+	sigaction(SIGTTOU, &sa, &saved_sa_ttou);
+
+	/* read key */
+	if (read(fd, &c, 1) == sizeof (c)) {
+		retval = c;
+	}
+
+	/* restore previous mode and echo setting */
+	do {
+		while ((tcsetattr(fd, TCSAFLUSH | TCSASOFT,
+		    &saved_term) < 0) && (errno != EINTR)) {
+			continue;
+		}
+	} while ((tcgetattr(fd, &term) == 0) &&
+	    (term.c_lflag != saved_term.c_lflag));
+
+	/* restore previous signal handlers */
+	sigaction(SIGHUP, &saved_sa_hup, &sa);
+	sigaction(SIGINT, &saved_sa_int, &sa);
+	sigaction(SIGQUIT, &saved_sa_quit, &sa);
+	sigaction(SIGPIPE, &saved_sa_pipe, &sa);
+	sigaction(SIGALRM, &saved_sa_alrm, &sa);
+	sigaction(SIGTERM, &saved_sa_term, &sa);
+	sigaction(SIGTSTP, &saved_sa_tstp, &sa);
+	sigaction(SIGTTIN, &saved_sa_ttin, &sa);
+	sigaction(SIGTTOU, &saved_sa_ttou, &sa);
+
+	/* erase prompt */
+	printf("\r%*s\r", (int)strlen(prompt), "");
+	fflush(stdout);
+
+	while ((close(fd) < 0) && (errno != EINTR)) {
+		continue;
+	}
+
+	/* re-raise signals received */
+	for (signo = 0; signo < _NSIG; signo++) {
+		if (signals[signo]) {
+			raise(signo);
+			if ((signo == SIGTSTP) || (signo == SIGTTIN) ||
+			    (signo == SIGTTOU)) {
+				need_restart = 1;
+			}
+		}
+	}
+	if (need_restart) {
+		goto restart;
+	}
+
+	return (retval);
+}
+
+struct pager *
+pager_create(FILE *fp)
+{
+	struct pager	*pager;
+
+	pager = xmalloc(sizeof (struct pager));
+	TAILQ_INIT(&pager->lines_head);
+	pager->fp = fp;
+	pager->buf_size = BUFSIZ;
+	pager->buf = xmalloc(BUFSIZ);
+
+	return (pager);
+}
+
+void
+pager_destroy(struct pager *pager)
+{
+	struct lines_entry *entry;
+	struct lines_entry *entry_tmp;
+
+	if (pager == NULL) {
+		return;
+	}
+
+	TAILQ_FOREACH_SAFE(entry, &pager->lines_head, entry, entry_tmp) {
+		TAILQ_REMOVE(&pager->lines_head, entry, entry);
+		free(entry->line);
+		free(entry);
+	}
+	free(pager->buf);
+	free(pager);
+}
+
+int
+pager_vprintf(struct pager *pager, const char *fmt, va_list args)
+{
+	int		len;
+	va_list		args_new;
+	char		*p;
+	size_t		line_len;
+	struct lines_entry *entry;
+	size_t		line_offset;
+
+	va_copy(args_new, args);
+
+	/* format multibyte string in buffer */
+	len = vsnprintf(NULL, 0, fmt, args);
+	if (len < 0) {
+		err(1, "vsnprintf");
+	}
+
+	if (pager->buf_size < (size_t)len + 1) {
+		pager->buf_size = len + 1;
+		pager->buf = xrealloc(pager->buf, pager->buf_size);
+	}
+
+	len = vsnprintf(pager->buf, pager->buf_size, fmt, args_new);
+	if (len < 0) {
+		err(1, "vsnprintf");
+	}
+
+	/* split buffer into lines */
+	for (p = pager->buf; *p != '\0'; p += line_len) {
+		line_len = strcspn(p, "\n");
+		if (p[line_len] == '\n') {
+			line_len++;
+		}
+
+		entry = TAILQ_LAST(&pager->lines_head, lines_head);
+		line_offset = (entry != NULL) ? strlen(entry->line) : 0;
+		if ((entry != NULL) && (entry->line[line_offset - 1] != '\n')) {
+			/*
+			 * append to the last line if it doesn't end with a
+			 * newline
+			 */
+			entry->line = xrealloc(entry->line, line_offset +
+			    line_len + 1);
+			memcpy(entry->line + line_offset, p, line_len);
+			entry->line[line_offset + line_len] = '\0';
+		} else {
+			entry = xmalloc(sizeof (struct lines_entry));
+			entry->line = xmalloc(line_len + 1);
+			memcpy(entry->line, p, line_len);
+			entry->line[line_len] = '\0';
+			TAILQ_INSERT_TAIL(&pager->lines_head, entry, entry);
+		}
+	}
+
+	va_end(args_new);
+
+	return (len);
+}
+
+int
+pager_printf(struct pager *pager, const char *fmt, ...)
+{
+	int	len;
+	va_list	args;
+
+	va_start(args, fmt);
+	len = pager_vprintf(pager, fmt, args);
+	va_end(args);
+
+	return (len);
+}
+
+static unsigned int
+mbsnprint(unsigned int cols, const char *s, FILE *fp)
+{
+	const char	*p = s;
+	unsigned int	col = 0;
+	int		mb_len;
+	wchar_t		wc;
+	int		width;
+	char		mb_buf[MB_LEN_MAX];
+
+	while ((*p != '\n') && (*p != '\0') && (col < cols)) {
+		mb_len = mbtowc(&wc, p, MB_CUR_MAX);
+		if (mb_len != -1) {
+			width = wcwidth(wc);
+			if (width != -1) {
+				if (snprintf(mb_buf, sizeof (mb_buf), "%.*s",
+				    mb_len, p) != mb_len) {
+					err(1, "snprintf");
+				}
+			} else {
+				/*
+				 * non-printable character, print
+				 * replacement character
+				 */
+				width = 1;
+				if (snprintf(mb_buf, sizeof (mb_buf),
+				    "\357\277\275") != 3) {
+					err(1, "snprintf");
+				}
+			}
+		} else {
+			/*
+			 * decoding failed, reset state and skip one
+			 * byte
+			 */
+			mbtowc(NULL, NULL, MB_CUR_MAX);
+			mb_len = 1;
+
+			/* print replacement character */
+			width = 1;
+			if (snprintf(mb_buf, sizeof (mb_buf),
+			    "\357\277\275") != 3) {
+				err(1, "snprintf");
+			}
+		}
+
+		p += mb_len;
+		col += width;
+		if (col <= cols) {
+			if (fputs(mb_buf, fp) == EOF) {
+				err(1, "fputs");
+			}
+		}
+	}
+
+	fputc('\n', fp);
+	fflush(fp);
+
+	return (col);
+}
+
+void
+pager_show(struct pager *pager)
+{
+	int		is_interactive;
+	unsigned int	rows = 24;
+	unsigned int	cols = 80;
+#ifdef	TIOCGWINSZ
+	struct winsize	ws;
+#endif /* TIOCGWINSZ */
+	unsigned int	row = 0;
+	struct lines_entry *entry;
+
+	is_interactive = (isatty(STDIN_FILENO) && (pager->fp == stdout));
+
+#ifdef	TIOCGWINSZ
+	if (is_interactive) {
+		/* determine terminal size */
+		if (ioctl(fileno(pager->fp), TIOCGWINSZ, &ws) == 0) {
+			rows = (ws.ws_row > 0) ? ws.ws_row : rows;
+			cols = (ws.ws_col > 0) ? ws.ws_col : cols;
+		}
+	}
+#endif /* TIOCGWINSZ */
+
+	TAILQ_FOREACH(entry, &pager->lines_head, entry) {
+		if (is_interactive) {
+			mbsnprint(cols, entry->line, pager->fp);
+			row++;
+			if (row == rows - 1) {
+				getch_prompt("--More--");
+				row = 0;
+			}
+		} else {
+			fprintf(pager->fp, "%s", entry->line);
+			fflush(pager->fp);
+		}
+	}
+}