view pager.c @ 18:1e39a251cbe9

Use libtecla for interactive input
author Guido Berhoerster <guido+pwm@berhoerster.name>
date Thu, 24 Aug 2017 13:10:56 +0200
parents a08ef0674d8e
children 5c6155c8e9b6
line wrap: on
line source

/*
 * 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 <libtecla.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"

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 int
getch_prompt(GetLine *gl, const char *prompt)
{
	int	c;
	int	saved_echo_mode;

	/* prompt with echo off */
	saved_echo_mode = gl_echo_mode(gl, -1);
	gl_echo_mode(gl, 0);
	c = gl_query_char(gl, prompt, '\0');
	gl_echo_mode(gl, saved_echo_mode);

	/* erase prompt */
	printf("\r%*s\r", (int)strlen(prompt), "");

	return (c);
}

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;
	GetLine		*gl;
	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 */

	gl = new_GetLine(10, 0);
	if (gl == NULL) {
		err(1, "new_GetLine");
	}

	TAILQ_FOREACH(entry, &pager->lines_head, entry) {
		if (is_interactive) {
			mbsnprint(cols, entry->line, pager->fp);
			row++;
			if ((TAILQ_NEXT(entry, entry) != NULL) &&
			    (row == rows - 1)) {
				getch_prompt(gl, "--More--");
				row = 0;
			}
		} else {
			fprintf(pager->fp, "%s", entry->line);
			fflush(pager->fp);
		}
	}

	del_GetLine(gl);
}