changeset 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 a07665727c19
children 1e39a251cbe9
files Makefile cmd.c compat.h compat/queue.h pager.c pager.h pwm.1.xml
diffstat 7 files changed, 1041 insertions(+), 54 deletions(-) [+]
line wrap: on
line diff
--- a/Makefile	Tue Aug 08 10:47:04 2017 +0200
+++ b/Makefile	Sat Aug 12 10:41:52 2017 +0200
@@ -81,6 +81,7 @@
   HAVE_ASPRINTF ?=	1
   HAVE_ERR_H ?=		1
   HAVE_GETRANDOM ?=	0
+  HAVE_SYS_QUEUE_H ?=	0
   HAVE_READPASSPHRASE_H ?= 0
   HAVE_SETPROGNAME ?=	0
   HAVE_SYS_TREE_H ?=	0
@@ -89,6 +90,7 @@
   HAVE_ASPRINTF ?=	1
   HAVE_ERR_H ?=		1
   HAVE_GETRANDOM ?=	0
+  HAVE_SYS_QUEUE_H ?=	1
   HAVE_READPASSPHRASE_H ?= 1
   HAVE_SETPROGNAME ?=	1
   HAVE_SYS_TREE_H ?=	1
@@ -97,6 +99,7 @@
   HAVE_ASPRINTF ?=	1
   HAVE_ERR_H ?=		1
   HAVE_GETRANDOM ?=	0
+  HAVE_SYS_QUEUE_H ?=	1
   HAVE_READPASSPHRASE_H ?= 0
   HAVE_SYS_TREE_H ?=	1
   HAVE_SETPROGNAME ?=	1
@@ -105,6 +108,7 @@
   HAVE_ASPRINTF ?=	1
   HAVE_ERR_H ?=		1
   HAVE_GETRANDOM ?=	0
+  HAVE_SYS_QUEUE_H ?=	1
   HAVE_READPASSPHRASE_H ?= 1
   HAVE_SYS_TREE_H ?=	1
   HAVE_SETPROGNAME ?=	1
@@ -120,6 +124,7 @@
     HAVE_ERR_H ?=	1
     HAVE_GETRANDOM ?=	1
   endif
+  HAVE_SYS_QUEUE_H ?=	0
   HAVE_READPASSPHRASE_H ?= 0
   HAVE_SYS_TREE_H ?=	0
   HAVE_SETPROGNAME ?=	0
@@ -128,12 +133,14 @@
   HAVE_ASPRINTF ?=	0
   HAVE_ERR_H ?=		0
   HAVE_GETRANDOM ?=	0
+  HAVE_SYS_QUEUE_H ?=	0
   HAVE_READPASSPHRASE_H ?= 0
   HAVE_SETPROGNAME ?=	0
   HAVE_SYS_TREE_H ?=	0
 endif
 
 OBJS =	cmd.o \
+	pager.o \
 	pw.o \
 	pwfile.o \
 	pwm.o \
--- a/cmd.c	Tue Aug 08 10:47:04 2017 +0200
+++ b/cmd.c	Sat Aug 12 10:41:52 2017 +0200
@@ -39,6 +39,7 @@
 #include <unistd.h>
 
 #include "cmd.h"
+#include "pager.h"
 #include "pw.h"
 #include "pwfile.h"
 #include "util.h"
@@ -236,6 +237,7 @@
 cmd_info(struct pwm_ctx *ctx, int argc, char *argv[])
 {
 	struct metadata	*metadata;
+	struct pager	*pager;
 	struct tm	*tm;
 	char		timebuf[TIME_SIZE];
 
@@ -244,19 +246,23 @@
 	}
 
 	metadata = pwfile_get_metadata(ctx);
-	printf("Format:      0x%04x\n", metadata->version);
+
+	pager = pager_create(stdout);
+	pager_printf(pager, "Format:      0x%04x\n", metadata->version);
 	if (metadata->user != NULL) {
-		printf("User:        %s\n", metadata->user);
+		pager_printf(pager, "User:        %s\n", metadata->user);
 	}
 	if (metadata->user != NULL) {
-		printf("Host:        %s\n", metadata->host);
+		pager_printf(pager, "Host:        %s\n", metadata->host);
 	}
 	if (metadata->user != NULL) {
-		printf("Application: %s\n", metadata->application);
+		pager_printf(pager, "Application: %s\n", metadata->application);
 	}
 	tm = gmtime(&metadata->timestamp);
 	strftime(timebuf, sizeof (timebuf), TIME_FORMAT, tm);
-	printf("Last Saved:  %s\n", timebuf);
+	pager_printf(pager, "Last Saved:  %s\n", timebuf);
+	pager_show(pager);
+	pager_destroy(pager);
 
 	pwfile_destroy_metadata(metadata);
 
@@ -279,6 +285,7 @@
 	int		errcode;
 	char		*errbuf;
 	size_t		errbuf_size;
+	struct pager	*pager = NULL;
 	union list_item	**list = NULL;
 	size_t		j;
 	struct record	*record;
@@ -334,10 +341,11 @@
 		}
 	}
 
+	pager = pager_create(stdout);
 	list = pwfile_create_list(ctx);
 	for (j = 0; list[j] != NULL; j++) {
 		if (list[j]->any.type == ITEM_TYPE_GROUP) {
-			printf("[%s]\n", list[j]->group.group);
+			pager_printf(pager, "[%s]\n", list[j]->group.group);
 		} else {
 			record = pwfile_get_record(ctx, list[j]->record.id);
 			if (((group_re == NULL) || (regexec(group_re,
@@ -350,16 +358,21 @@
 			    record->notes, 0, NULL, 0) == 0)) &&
 			    ((url_re == NULL) || (regexec(url_re,
 			    record->url, 0, NULL, 0) == 0))) {
-				printf("%4u %s\n", list[j]->record.id,
+				pager_printf(pager, "%4u %s\n",
+				    list[j]->record.id,
 				    (list[j]->record.title != NULL) ?
 				    list[j]->record.title : "");
 			}
 			pwfile_destroy_record(record);
 		}
 	}
+	pager_show(pager);
+
 	retval = CMD_OK;
 
 out:
+	pager_destroy(pager);
+
 	if (group_re != NULL) {
 		regfree(group_re);
 		free(group_re);
@@ -383,6 +396,7 @@
 
 	pwfile_destroy_list(list);
 
+
 	return (retval);
 }
 
@@ -656,76 +670,58 @@
 	return (CMD_OK);
 }
 
-static int
-print_field(const char *label, const char *value, int show_label, FILE *fp)
-{
-	fprintf(fp, "%s%s\n", show_label ? label : "", (value != NULL) ?
-	    value : "");
-	if (ferror(fp)) {
-		warn("fprintf");
-		return (-1);
-	}
-	return (0);
-}
-
 static void
 print_record(struct record *record, int fields[], int show_labels, FILE *fp)
 {
+	struct pager	*pager;
 	struct tm	*tm;
 	char		timebuf[TIME_SIZE];
 
+	pager = pager_create(fp);
 	if (fields[FIELD_TITLE]) {
-		if (print_field(field_labels[FIELD_TITLE], record->title,
-		    show_labels, fp) != 0) {
-			return;
-		}
+		pager_printf(pager, "%s%s\n", show_labels ?
+		    field_labels[FIELD_TITLE] : "", (record->title != NULL) ?
+		    record->title : "");
 	}
 	if (fields[FIELD_GROUP]) {
-		if (print_field(field_labels[FIELD_GROUP], record->group,
-		    show_labels, fp)) {
-			return;
-		}
+		pager_printf(pager, "%s%s\n", show_labels ?
+		    field_labels[FIELD_GROUP] : "", (record->group != NULL) ?
+		    record->group : "");
 	}
 	if (fields[FIELD_USERNAME]) {
-		if (print_field(field_labels[FIELD_USERNAME], record->username,
-		    show_labels, fp)) {
-			return;
-		}
+		pager_printf(pager, "%s%s\n", show_labels ?
+		    field_labels[FIELD_USERNAME] : "",
+		    (record->username != NULL) ?  record->username : "");
 	}
 	if (fields[FIELD_PASSWORD]) {
-		if (print_field(field_labels[FIELD_PASSWORD], record->password,
-		    show_labels, fp)) {
-			return;
-		}
+		pager_printf(pager, "%s%s\n", show_labels ?
+		    field_labels[FIELD_PASSWORD] : "",
+		    (record->password != NULL) ?  record->password : "");
 	}
 	if (fields[FIELD_NOTES]) {
-		if (print_field(field_labels[FIELD_NOTES], record->notes,
-		    show_labels, fp)) {
-			return;
-		}
+		pager_printf(pager, "%s%s\n", show_labels ?
+		    field_labels[FIELD_NOTES] : "", (record->notes != NULL) ?
+		    record->notes : "");
 	}
 	if (fields[FIELD_URL]) {
-		if (print_field(field_labels[FIELD_URL], record->url,
-		    show_labels, fp)) {
-			return;
-		}
+		pager_printf(pager, "%s%s\n", show_labels ?
+		    field_labels[FIELD_URL] : "", (record->url != NULL) ?
+		    record->url : "");
 	}
 	if (fields[FIELD_CTIME]) {
 		tm = gmtime(&record->ctime);
 		strftime(timebuf, sizeof (timebuf), TIME_FORMAT, tm);
-		if (print_field(field_labels[FIELD_CTIME], timebuf,
-		    show_labels, fp)) {
-			return;
-		}
+		pager_printf(pager, "%s%s\n", show_labels ?
+		    field_labels[FIELD_CTIME] : "", timebuf);
 	}
 	if (fields[FIELD_MTIME]) {
 		tm = gmtime(&record->mtime);
 		strftime(timebuf, sizeof (timebuf), TIME_FORMAT, tm);
-		if (print_field(field_labels[FIELD_MTIME], timebuf,
-		    show_labels, fp)) {
-			return;
-		}
+		pager_printf(pager, "%s%s\n", show_labels ?
+		    field_labels[FIELD_MTIME] : "", timebuf);
 	}
+	pager_show(pager);
+	pager_destroy(pager);
 }
 
 static enum cmd_return
@@ -909,27 +905,31 @@
 static enum cmd_return
 cmd_help(struct pwm_ctx *ctx, int argc, char *argv[])
 {
+	struct pager	*pager;
 	struct cmd	*cmd;
 
 	if (argc > 2) {
 		return (CMD_USAGE);
 	}
 
+	pager = pager_create(stdout);
 	if (argc == 2) {
 		for (cmd = cmds; cmd->cmd_func != NULL; cmd++) {
 			if ((strcmp(argv[1], cmd->abbrev_cmd) == 0) ||
 			    (strcmp(argv[1], cmd->full_cmd) == 0)) {
-				printf("%s\n", cmd->usage);
+				pager_printf(pager, "%s\n", cmd->usage);
 				break;
 			}
 		}
 	} else {
 		printf("Commands:\n");
 		for (cmd = cmds; cmd->cmd_func != NULL; cmd++) {
-			printf("%-2s %-16s %s\n", cmd->abbrev_cmd,
+			pager_printf(pager, "%-2s %-16s %s\n", cmd->abbrev_cmd,
 			    cmd->full_cmd, cmd->description);
 		}
 	}
+	pager_show(pager);
+	pager_destroy(pager);
 
 	return (CMD_OK);
 }
--- a/compat.h	Tue Aug 08 10:47:04 2017 +0200
+++ b/compat.h	Sat Aug 12 10:41:52 2017 +0200
@@ -39,6 +39,10 @@
 #include "compat/getentropy.h"
 #endif /* !HAVE_GETENTROPY */
 
+#ifndef	HAVE_SYS_QUEUE_H
+#include "compat/queue.h"
+#endif /* !HAVE_SYS_QUEUE_H */
+
 #ifndef	HAVE_READPASSPHRASE_H
 #include "compat/readpassphrase.h"
 #endif /* !HAVE_READPASSPHRASE_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/compat/queue.h	Sat Aug 12 10:41:52 2017 +0200
@@ -0,0 +1,535 @@
+/*	$OpenBSD: queue.h,v 1.44 2016/09/09 20:31:46 millert Exp $	*/
+/*	$NetBSD: queue.h,v 1.11 1996/05/16 05:17:14 mycroft Exp $	*/
+
+/*
+ * Copyright (c) 1991, 1993
+ *	The Regents of the University of California.  All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of the University nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software
+ *    without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ *
+ *	@(#)queue.h	8.5 (Berkeley) 8/20/94
+ */
+
+#ifndef	_SYS_QUEUE_H_
+#define	_SYS_QUEUE_H_
+
+#include <stddef.h>
+
+/*
+ * This file defines five types of data structures: singly-linked lists,
+ * lists, simple queues, tail queues and XOR simple queues.
+ *
+ *
+ * A singly-linked list is headed by a single forward pointer. The elements
+ * are singly linked for minimum space and pointer manipulation overhead at
+ * the expense of O(n) removal for arbitrary elements. New elements can be
+ * added to the list after an existing element or at the head of the list.
+ * Elements being removed from the head of the list should use the explicit
+ * macro for this purpose for optimum efficiency. A singly-linked list may
+ * only be traversed in the forward direction.  Singly-linked lists are ideal
+ * for applications with large datasets and few or no removals or for
+ * implementing a LIFO queue.
+ *
+ * A list is headed by a single forward pointer (or an array of forward
+ * pointers for a hash table header). The elements are doubly linked
+ * so that an arbitrary element can be removed without a need to
+ * traverse the list. New elements can be added to the list before
+ * or after an existing element or at the head of the list. A list
+ * may only be traversed in the forward direction.
+ *
+ * A simple queue is headed by a pair of pointers, one to the head of the
+ * list and the other to the tail of the list. The elements are singly
+ * linked to save space, so elements can only be removed from the
+ * head of the list. New elements can be added to the list before or after
+ * an existing element, at the head of the list, or at the end of the
+ * list. A simple queue may only be traversed in the forward direction.
+ *
+ * A tail queue is headed by a pair of pointers, one to the head of the
+ * list and the other to the tail of the list. The elements are doubly
+ * linked so that an arbitrary element can be removed without a need to
+ * traverse the list. New elements can be added to the list before or
+ * after an existing element, at the head of the list, or at the end of
+ * the list. A tail queue may be traversed in either direction.
+ *
+ * An XOR simple queue is used in the same way as a regular simple queue.
+ * The difference is that the head structure also includes a "cookie" that
+ * is XOR'd with the queue pointer (first, last or next) to generate the
+ * real pointer value.
+ *
+ * For details on the use of these macros, see the queue(3) manual page.
+ */
+
+#if defined(QUEUE_MACRO_DEBUG) || (defined(_KERNEL) && defined(DIAGNOSTIC))
+#define _Q_INVALIDATE(a) (a) = ((void *)-1)
+#else
+#define _Q_INVALIDATE(a)
+#endif
+
+/*
+ * Singly-linked List definitions.
+ */
+#define SLIST_HEAD(name, type)						\
+struct name {								\
+	struct type *slh_first;	/* first element */			\
+}
+
+#define	SLIST_HEAD_INITIALIZER(head)					\
+	{ NULL }
+
+#define SLIST_ENTRY(type)						\
+struct {								\
+	struct type *sle_next;	/* next element */			\
+}
+
+/*
+ * Singly-linked List access methods.
+ */
+#define	SLIST_FIRST(head)	((head)->slh_first)
+#define	SLIST_END(head)		NULL
+#define	SLIST_EMPTY(head)	(SLIST_FIRST(head) == SLIST_END(head))
+#define	SLIST_NEXT(elm, field)	((elm)->field.sle_next)
+
+#define	SLIST_FOREACH(var, head, field)					\
+	for((var) = SLIST_FIRST(head);					\
+	    (var) != SLIST_END(head);					\
+	    (var) = SLIST_NEXT(var, field))
+
+#define	SLIST_FOREACH_SAFE(var, head, field, tvar)			\
+	for ((var) = SLIST_FIRST(head);				\
+	    (var) && ((tvar) = SLIST_NEXT(var, field), 1);		\
+	    (var) = (tvar))
+
+/*
+ * Singly-linked List functions.
+ */
+#define	SLIST_INIT(head) {						\
+	SLIST_FIRST(head) = SLIST_END(head);				\
+}
+
+#define	SLIST_INSERT_AFTER(slistelm, elm, field) do {			\
+	(elm)->field.sle_next = (slistelm)->field.sle_next;		\
+	(slistelm)->field.sle_next = (elm);				\
+} while (0)
+
+#define	SLIST_INSERT_HEAD(head, elm, field) do {			\
+	(elm)->field.sle_next = (head)->slh_first;			\
+	(head)->slh_first = (elm);					\
+} while (0)
+
+#define	SLIST_REMOVE_AFTER(elm, field) do {				\
+	(elm)->field.sle_next = (elm)->field.sle_next->field.sle_next;	\
+} while (0)
+
+#define	SLIST_REMOVE_HEAD(head, field) do {				\
+	(head)->slh_first = (head)->slh_first->field.sle_next;		\
+} while (0)
+
+#define SLIST_REMOVE(head, elm, type, field) do {			\
+	if ((head)->slh_first == (elm)) {				\
+		SLIST_REMOVE_HEAD((head), field);			\
+	} else {							\
+		struct type *curelm = (head)->slh_first;		\
+									\
+		while (curelm->field.sle_next != (elm))			\
+			curelm = curelm->field.sle_next;		\
+		curelm->field.sle_next =				\
+		    curelm->field.sle_next->field.sle_next;		\
+	}								\
+	_Q_INVALIDATE((elm)->field.sle_next);				\
+} while (0)
+
+/*
+ * List definitions.
+ */
+#define LIST_HEAD(name, type)						\
+struct name {								\
+	struct type *lh_first;	/* first element */			\
+}
+
+#define LIST_HEAD_INITIALIZER(head)					\
+	{ NULL }
+
+#define LIST_ENTRY(type)						\
+struct {								\
+	struct type *le_next;	/* next element */			\
+	struct type **le_prev;	/* address of previous next element */	\
+}
+
+/*
+ * List access methods.
+ */
+#define	LIST_FIRST(head)		((head)->lh_first)
+#define	LIST_END(head)			NULL
+#define	LIST_EMPTY(head)		(LIST_FIRST(head) == LIST_END(head))
+#define	LIST_NEXT(elm, field)		((elm)->field.le_next)
+
+#define LIST_FOREACH(var, head, field)					\
+	for((var) = LIST_FIRST(head);					\
+	    (var)!= LIST_END(head);					\
+	    (var) = LIST_NEXT(var, field))
+
+#define	LIST_FOREACH_SAFE(var, head, field, tvar)			\
+	for ((var) = LIST_FIRST(head);				\
+	    (var) && ((tvar) = LIST_NEXT(var, field), 1);		\
+	    (var) = (tvar))
+
+/*
+ * List functions.
+ */
+#define	LIST_INIT(head) do {						\
+	LIST_FIRST(head) = LIST_END(head);				\
+} while (0)
+
+#define LIST_INSERT_AFTER(listelm, elm, field) do {			\
+	if (((elm)->field.le_next = (listelm)->field.le_next) != NULL)	\
+		(listelm)->field.le_next->field.le_prev =		\
+		    &(elm)->field.le_next;				\
+	(listelm)->field.le_next = (elm);				\
+	(elm)->field.le_prev = &(listelm)->field.le_next;		\
+} while (0)
+
+#define	LIST_INSERT_BEFORE(listelm, elm, field) do {			\
+	(elm)->field.le_prev = (listelm)->field.le_prev;		\
+	(elm)->field.le_next = (listelm);				\
+	*(listelm)->field.le_prev = (elm);				\
+	(listelm)->field.le_prev = &(elm)->field.le_next;		\
+} while (0)
+
+#define LIST_INSERT_HEAD(head, elm, field) do {				\
+	if (((elm)->field.le_next = (head)->lh_first) != NULL)		\
+		(head)->lh_first->field.le_prev = &(elm)->field.le_next;\
+	(head)->lh_first = (elm);					\
+	(elm)->field.le_prev = &(head)->lh_first;			\
+} while (0)
+
+#define LIST_REMOVE(elm, field) do {					\
+	if ((elm)->field.le_next != NULL)				\
+		(elm)->field.le_next->field.le_prev =			\
+		    (elm)->field.le_prev;				\
+	*(elm)->field.le_prev = (elm)->field.le_next;			\
+	_Q_INVALIDATE((elm)->field.le_prev);				\
+	_Q_INVALIDATE((elm)->field.le_next);				\
+} while (0)
+
+#define LIST_REPLACE(elm, elm2, field) do {				\
+	if (((elm2)->field.le_next = (elm)->field.le_next) != NULL)	\
+		(elm2)->field.le_next->field.le_prev =			\
+		    &(elm2)->field.le_next;				\
+	(elm2)->field.le_prev = (elm)->field.le_prev;			\
+	*(elm2)->field.le_prev = (elm2);				\
+	_Q_INVALIDATE((elm)->field.le_prev);				\
+	_Q_INVALIDATE((elm)->field.le_next);				\
+} while (0)
+
+/*
+ * Simple queue definitions.
+ */
+#define SIMPLEQ_HEAD(name, type)					\
+struct name {								\
+	struct type *sqh_first;	/* first element */			\
+	struct type **sqh_last;	/* addr of last next element */		\
+}
+
+#define SIMPLEQ_HEAD_INITIALIZER(head)					\
+	{ NULL, &(head).sqh_first }
+
+#define SIMPLEQ_ENTRY(type)						\
+struct {								\
+	struct type *sqe_next;	/* next element */			\
+}
+
+/*
+ * Simple queue access methods.
+ */
+#define	SIMPLEQ_FIRST(head)	    ((head)->sqh_first)
+#define	SIMPLEQ_END(head)	    NULL
+#define	SIMPLEQ_EMPTY(head)	    (SIMPLEQ_FIRST(head) == SIMPLEQ_END(head))
+#define	SIMPLEQ_NEXT(elm, field)    ((elm)->field.sqe_next)
+
+#define SIMPLEQ_FOREACH(var, head, field)				\
+	for((var) = SIMPLEQ_FIRST(head);				\
+	    (var) != SIMPLEQ_END(head);					\
+	    (var) = SIMPLEQ_NEXT(var, field))
+
+#define	SIMPLEQ_FOREACH_SAFE(var, head, field, tvar)			\
+	for ((var) = SIMPLEQ_FIRST(head);				\
+	    (var) && ((tvar) = SIMPLEQ_NEXT(var, field), 1);		\
+	    (var) = (tvar))
+
+/*
+ * Simple queue functions.
+ */
+#define	SIMPLEQ_INIT(head) do {						\
+	(head)->sqh_first = NULL;					\
+	(head)->sqh_last = &(head)->sqh_first;				\
+} while (0)
+
+#define SIMPLEQ_INSERT_HEAD(head, elm, field) do {			\
+	if (((elm)->field.sqe_next = (head)->sqh_first) == NULL)	\
+		(head)->sqh_last = &(elm)->field.sqe_next;		\
+	(head)->sqh_first = (elm);					\
+} while (0)
+
+#define SIMPLEQ_INSERT_TAIL(head, elm, field) do {			\
+	(elm)->field.sqe_next = NULL;					\
+	*(head)->sqh_last = (elm);					\
+	(head)->sqh_last = &(elm)->field.sqe_next;			\
+} while (0)
+
+#define SIMPLEQ_INSERT_AFTER(head, listelm, elm, field) do {		\
+	if (((elm)->field.sqe_next = (listelm)->field.sqe_next) == NULL)\
+		(head)->sqh_last = &(elm)->field.sqe_next;		\
+	(listelm)->field.sqe_next = (elm);				\
+} while (0)
+
+#define SIMPLEQ_REMOVE_HEAD(head, field) do {			\
+	if (((head)->sqh_first = (head)->sqh_first->field.sqe_next) == NULL) \
+		(head)->sqh_last = &(head)->sqh_first;			\
+} while (0)
+
+#define SIMPLEQ_REMOVE_AFTER(head, elm, field) do {			\
+	if (((elm)->field.sqe_next = (elm)->field.sqe_next->field.sqe_next) \
+	    == NULL)							\
+		(head)->sqh_last = &(elm)->field.sqe_next;		\
+} while (0)
+
+#define SIMPLEQ_CONCAT(head1, head2) do {				\
+	if (!SIMPLEQ_EMPTY((head2))) {					\
+		*(head1)->sqh_last = (head2)->sqh_first;		\
+		(head1)->sqh_last = (head2)->sqh_last;			\
+		SIMPLEQ_INIT((head2));					\
+	}								\
+} while (0)
+
+/*
+ * XOR Simple queue definitions.
+ */
+#define XSIMPLEQ_HEAD(name, type)					\
+struct name {								\
+	struct type *sqx_first;	/* first element */			\
+	struct type **sqx_last;	/* addr of last next element */		\
+	unsigned long sqx_cookie;					\
+}
+
+#define XSIMPLEQ_ENTRY(type)						\
+struct {								\
+	struct type *sqx_next;	/* next element */			\
+}
+
+/*
+ * XOR Simple queue access methods.
+ */
+#define XSIMPLEQ_XOR(head, ptr)	    ((__typeof(ptr))((head)->sqx_cookie ^ \
+					(unsigned long)(ptr)))
+#define	XSIMPLEQ_FIRST(head)	    XSIMPLEQ_XOR(head, ((head)->sqx_first))
+#define	XSIMPLEQ_END(head)	    NULL
+#define	XSIMPLEQ_EMPTY(head)	    (XSIMPLEQ_FIRST(head) == XSIMPLEQ_END(head))
+#define	XSIMPLEQ_NEXT(head, elm, field)    XSIMPLEQ_XOR(head, ((elm)->field.sqx_next))
+
+
+#define XSIMPLEQ_FOREACH(var, head, field)				\
+	for ((var) = XSIMPLEQ_FIRST(head);				\
+	    (var) != XSIMPLEQ_END(head);				\
+	    (var) = XSIMPLEQ_NEXT(head, var, field))
+
+#define	XSIMPLEQ_FOREACH_SAFE(var, head, field, tvar)			\
+	for ((var) = XSIMPLEQ_FIRST(head);				\
+	    (var) && ((tvar) = XSIMPLEQ_NEXT(head, var, field), 1);	\
+	    (var) = (tvar))
+
+/*
+ * XOR Simple queue functions.
+ */
+#define	XSIMPLEQ_INIT(head) do {					\
+	arc4random_buf(&(head)->sqx_cookie, sizeof((head)->sqx_cookie)); \
+	(head)->sqx_first = XSIMPLEQ_XOR(head, NULL);			\
+	(head)->sqx_last = XSIMPLEQ_XOR(head, &(head)->sqx_first);	\
+} while (0)
+
+#define XSIMPLEQ_INSERT_HEAD(head, elm, field) do {			\
+	if (((elm)->field.sqx_next = (head)->sqx_first) ==		\
+	    XSIMPLEQ_XOR(head, NULL))					\
+		(head)->sqx_last = XSIMPLEQ_XOR(head, &(elm)->field.sqx_next); \
+	(head)->sqx_first = XSIMPLEQ_XOR(head, (elm));			\
+} while (0)
+
+#define XSIMPLEQ_INSERT_TAIL(head, elm, field) do {			\
+	(elm)->field.sqx_next = XSIMPLEQ_XOR(head, NULL);		\
+	*(XSIMPLEQ_XOR(head, (head)->sqx_last)) = XSIMPLEQ_XOR(head, (elm)); \
+	(head)->sqx_last = XSIMPLEQ_XOR(head, &(elm)->field.sqx_next);	\
+} while (0)
+
+#define XSIMPLEQ_INSERT_AFTER(head, listelm, elm, field) do {		\
+	if (((elm)->field.sqx_next = (listelm)->field.sqx_next) ==	\
+	    XSIMPLEQ_XOR(head, NULL))					\
+		(head)->sqx_last = XSIMPLEQ_XOR(head, &(elm)->field.sqx_next); \
+	(listelm)->field.sqx_next = XSIMPLEQ_XOR(head, (elm));		\
+} while (0)
+
+#define XSIMPLEQ_REMOVE_HEAD(head, field) do {				\
+	if (((head)->sqx_first = XSIMPLEQ_XOR(head,			\
+	    (head)->sqx_first)->field.sqx_next) == XSIMPLEQ_XOR(head, NULL)) \
+		(head)->sqx_last = XSIMPLEQ_XOR(head, &(head)->sqx_first); \
+} while (0)
+
+#define XSIMPLEQ_REMOVE_AFTER(head, elm, field) do {			\
+	if (((elm)->field.sqx_next = XSIMPLEQ_XOR(head,			\
+	    (elm)->field.sqx_next)->field.sqx_next)			\
+	    == XSIMPLEQ_XOR(head, NULL))				\
+		(head)->sqx_last = 					\
+		    XSIMPLEQ_XOR(head, &(elm)->field.sqx_next);		\
+} while (0)
+
+
+/*
+ * Tail queue definitions.
+ */
+#define TAILQ_HEAD(name, type)						\
+struct name {								\
+	struct type *tqh_first;	/* first element */			\
+	struct type **tqh_last;	/* addr of last next element */		\
+}
+
+#define TAILQ_HEAD_INITIALIZER(head)					\
+	{ NULL, &(head).tqh_first }
+
+#define TAILQ_ENTRY(type)						\
+struct {								\
+	struct type *tqe_next;	/* next element */			\
+	struct type **tqe_prev;	/* address of previous next element */	\
+}
+
+/*
+ * Tail queue access methods.
+ */
+#define	TAILQ_FIRST(head)		((head)->tqh_first)
+#define	TAILQ_END(head)			NULL
+#define	TAILQ_NEXT(elm, field)		((elm)->field.tqe_next)
+#define TAILQ_LAST(head, headname)					\
+	(*(((struct headname *)((head)->tqh_last))->tqh_last))
+/* XXX */
+#define TAILQ_PREV(elm, headname, field)				\
+	(*(((struct headname *)((elm)->field.tqe_prev))->tqh_last))
+#define	TAILQ_EMPTY(head)						\
+	(TAILQ_FIRST(head) == TAILQ_END(head))
+
+#define TAILQ_FOREACH(var, head, field)					\
+	for((var) = TAILQ_FIRST(head);					\
+	    (var) != TAILQ_END(head);					\
+	    (var) = TAILQ_NEXT(var, field))
+
+#define	TAILQ_FOREACH_SAFE(var, head, field, tvar)			\
+	for ((var) = TAILQ_FIRST(head);					\
+	    (var) != TAILQ_END(head) &&					\
+	    ((tvar) = TAILQ_NEXT(var, field), 1);			\
+	    (var) = (tvar))
+
+
+#define TAILQ_FOREACH_REVERSE(var, head, headname, field)		\
+	for((var) = TAILQ_LAST(head, headname);				\
+	    (var) != TAILQ_END(head);					\
+	    (var) = TAILQ_PREV(var, headname, field))
+
+#define	TAILQ_FOREACH_REVERSE_SAFE(var, head, headname, field, tvar)	\
+	for ((var) = TAILQ_LAST(head, headname);			\
+	    (var) != TAILQ_END(head) &&					\
+	    ((tvar) = TAILQ_PREV(var, headname, field), 1);		\
+	    (var) = (tvar))
+
+/*
+ * Tail queue functions.
+ */
+#define	TAILQ_INIT(head) do {						\
+	(head)->tqh_first = NULL;					\
+	(head)->tqh_last = &(head)->tqh_first;				\
+} while (0)
+
+#define TAILQ_INSERT_HEAD(head, elm, field) do {			\
+	if (((elm)->field.tqe_next = (head)->tqh_first) != NULL)	\
+		(head)->tqh_first->field.tqe_prev =			\
+		    &(elm)->field.tqe_next;				\
+	else								\
+		(head)->tqh_last = &(elm)->field.tqe_next;		\
+	(head)->tqh_first = (elm);					\
+	(elm)->field.tqe_prev = &(head)->tqh_first;			\
+} while (0)
+
+#define TAILQ_INSERT_TAIL(head, elm, field) do {			\
+	(elm)->field.tqe_next = NULL;					\
+	(elm)->field.tqe_prev = (head)->tqh_last;			\
+	*(head)->tqh_last = (elm);					\
+	(head)->tqh_last = &(elm)->field.tqe_next;			\
+} while (0)
+
+#define TAILQ_INSERT_AFTER(head, listelm, elm, field) do {		\
+	if (((elm)->field.tqe_next = (listelm)->field.tqe_next) != NULL)\
+		(elm)->field.tqe_next->field.tqe_prev =			\
+		    &(elm)->field.tqe_next;				\
+	else								\
+		(head)->tqh_last = &(elm)->field.tqe_next;		\
+	(listelm)->field.tqe_next = (elm);				\
+	(elm)->field.tqe_prev = &(listelm)->field.tqe_next;		\
+} while (0)
+
+#define	TAILQ_INSERT_BEFORE(listelm, elm, field) do {			\
+	(elm)->field.tqe_prev = (listelm)->field.tqe_prev;		\
+	(elm)->field.tqe_next = (listelm);				\
+	*(listelm)->field.tqe_prev = (elm);				\
+	(listelm)->field.tqe_prev = &(elm)->field.tqe_next;		\
+} while (0)
+
+#define TAILQ_REMOVE(head, elm, field) do {				\
+	if (((elm)->field.tqe_next) != NULL)				\
+		(elm)->field.tqe_next->field.tqe_prev =			\
+		    (elm)->field.tqe_prev;				\
+	else								\
+		(head)->tqh_last = (elm)->field.tqe_prev;		\
+	*(elm)->field.tqe_prev = (elm)->field.tqe_next;			\
+	_Q_INVALIDATE((elm)->field.tqe_prev);				\
+	_Q_INVALIDATE((elm)->field.tqe_next);				\
+} while (0)
+
+#define TAILQ_REPLACE(head, elm, elm2, field) do {			\
+	if (((elm2)->field.tqe_next = (elm)->field.tqe_next) != NULL)	\
+		(elm2)->field.tqe_next->field.tqe_prev =		\
+		    &(elm2)->field.tqe_next;				\
+	else								\
+		(head)->tqh_last = &(elm2)->field.tqe_next;		\
+	(elm2)->field.tqe_prev = (elm)->field.tqe_prev;			\
+	*(elm2)->field.tqe_prev = (elm2);				\
+	_Q_INVALIDATE((elm)->field.tqe_prev);				\
+	_Q_INVALIDATE((elm)->field.tqe_next);				\
+} while (0)
+
+#define TAILQ_CONCAT(head1, head2, field) do {				\
+	if (!TAILQ_EMPTY(head2)) {					\
+		*(head1)->tqh_last = (head2)->tqh_first;		\
+		(head2)->tqh_first->field.tqe_prev = (head1)->tqh_last;	\
+		(head1)->tqh_last = (head2)->tqh_last;			\
+		TAILQ_INIT((head2));					\
+	}								\
+} while (0)
+
+#endif	/* !_SYS_QUEUE_H_ */
--- /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);
+		}
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pager.h	Sat Aug 12 10:41:52 2017 +0200
@@ -0,0 +1,38 @@
+/*
+ * 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.
+ */
+
+
+#ifndef	PAGER_H
+#define	PAGER_H
+
+#include <stdarg.h>
+
+struct pager;
+
+struct pager *	pager_create(FILE *fp);
+void		pager_destroy(struct pager *);
+int		pager_vprintf(struct pager *, const char *, va_list);
+int		pager_printf(struct pager *, const char *, ...);
+void		pager_show(struct pager *);
+
+#endif /* !PAGER_H */
--- a/pwm.1.xml	Tue Aug 08 10:47:04 2017 +0200
+++ b/pwm.1.xml	Sat Aug 12 10:41:52 2017 +0200
@@ -34,7 +34,7 @@
       <email>guido+pwm@berhoerster.name</email>
       <personblurb/>
     </author>
-    <date>8 August, 2017</date>
+    <date>12 August, 2017</date>
   </info>
   <refmeta>
     <refentrytitle>pwm</refentrytitle>
@@ -85,6 +85,9 @@
       more space characters and the field's verbatim content to the standard
       output stream. Field content may contain newlines, non-printable and/or
       control characters.</para>
+      <para>If running in interactive mode, the <command>list</command>,
+      <command>show</command> and <command>info</command> will display
+      the results on a page-by-page basis using an internal pager.</para>
       <para>The <command>pipe</command> prints the verbatim field content to the
       standard input stream of the given command.</para>
       <para>Error messages are printed to the standard error stream.</para>