/*	$OpenBSD: tty.c,v 1.39 2021/03/20 09:00:49 lum Exp $	*/

/* This file is in the public domain. */

/*
 * Terminfo display driver
 *
 * Terminfo is a terminal information database and routines to describe
 * terminals on most modern UNIX systems.  Many other systems have adopted
 * this as a reasonable way to allow for widely varying and ever changing
 * varieties of terminal types.	 This should be used where practical.
 */
/*
 * Known problems: If you have a terminal with no clear to end of screen and
 * memory of lines below the ones visible on the screen, display will be
 * wrong in some cases.  I doubt that any such terminal was ever made, but I
 * thought everyone with delete line would have clear to end of screen too...
 *
 * Code for terminals without clear to end of screen and/or clear to end of line
 * has not been extensively tested.
 *
 * Cost calculations are very rough.  Costs of insert/delete line may be far
 * from the truth.  This is accentuated by display.c not knowing about
 * multi-line insert/delete.
 *
 * Using scrolling region vs insert/delete line should probably be based on cost
 * rather than the assumption that scrolling region operations look better.
 */

#include <sys/ioctl.h>
#include <sys/queue.h>
#include <sys/types.h>
#include <sys/time.h>
#include <signal.h>
#include <stdio.h>
#include <term.h>
#include <unistd.h>

#include "def.h"

static int	 charcost(const char *);

static int	 cci;
static int	 insdel;	/* Do we have both insert & delete line? */
static char	*scroll_fwd;	/* How to scroll forward. */

static void	 winchhandler(int);

volatile sig_atomic_t	 winch_flag;
int			 tceeol;
int			 tcinsl;
int			 tcdell;

/* ARGSUSED */
static void
winchhandler(int sig)
{
	winch_flag = 1;
}

/*
 * Initialize the terminal when the editor
 * gets started up.
 */
void
ttinit(void)
{
	char *tty;
	int errret;

	if (batch == 1)
		tty = "pty";
	else
		tty = NULL;

	if (setupterm(tty, STDOUT_FILENO, &errret))
		panic("Terminal setup failed");

	signal(SIGWINCH, winchhandler);
	signal(SIGCONT, winchhandler);
	siginterrupt(SIGWINCH, 1);

	scroll_fwd = scroll_forward;
	if (scroll_fwd == NULL || *scroll_fwd == '\0') {
		/* this is what GNU Emacs does */
		scroll_fwd = parm_down_cursor;
		if (scroll_fwd == NULL || *scroll_fwd == '\0')
			scroll_fwd = curbp->b_nlchr;
	}

	if (cursor_address == NULL || cursor_up == NULL)
		panic("This terminal is too stupid to run mg");

	/* set nrow & ncol */
	ttresize();

	if (!clr_eol)
		tceeol = ncol;
	else
		tceeol = charcost(clr_eol);

	/* Estimate cost of inserting a line */
	if (change_scroll_region && scroll_reverse)
		tcinsl = charcost(change_scroll_region) * 2 +
		    charcost(scroll_reverse);
	else if (parm_insert_line)
		tcinsl = charcost(parm_insert_line);
	else if (insert_line)
		tcinsl = charcost(insert_line);
	else
		/* make this cost high enough */
		tcinsl = nrow * ncol;

	/* Estimate cost of deleting a line */
	if (change_scroll_region)
		tcdell = charcost(change_scroll_region) * 2 +
		    charcost(scroll_fwd);
	else if (parm_delete_line)
		tcdell = charcost(parm_delete_line);
	else if (delete_line)
		tcdell = charcost(delete_line);
	else
		/* make this cost high enough */
		tcdell = nrow * ncol;

	/* Flag to indicate that we can both insert and delete lines */
	insdel = (insert_line || parm_insert_line) &&
	    (delete_line || parm_delete_line);

	if (enter_ca_mode)
		/* enter application mode */
		putpad(enter_ca_mode, 1);

	ttresize();
}

/*
 * Re-initialize the terminal when the editor is resumed.
 * The keypad_xmit doesn't really belong here but...
 */
void
ttreinit(void)
{
	/* check if file was modified while we were gone */
	if (fchecktime(curbp) != TRUE) {
		curbp->b_flag |= BFDIRTY;
	}

	if (enter_ca_mode)
		/* enter application mode */
		putpad(enter_ca_mode, 1);

	if (keypad_xmit)
		/* turn on keypad */
		putpad(keypad_xmit, 1);

	ttresize();
}

/*
 * Clean up the terminal, in anticipation of a return to the command
 * interpreter. This is a no-op on the ANSI display. On the SCALD display,
 * it sets the window back to half screen scrolling. Perhaps it should
 * query the display for the increment, and put it back to what it was.
 */
void
tttidy(void)
{
	ttykeymaptidy();

	/* set the term back to normal mode */
	if (exit_ca_mode)
		putpad(exit_ca_mode, 1);
}

/*
 * Move the cursor to the specified origin 0 row and column position. Try to
 * optimize out extra moves; redisplay may have left the cursor in the right
 * location last time!
 */
void
ttmove(int row, int col)
{
	if (ttrow != row || ttcol != col) {
		putpad(tgoto(cursor_address, col, row), 1);
		ttrow = row;
		ttcol = col;
	}
}

/*
 * Erase to end of line.
 */
void
tteeol(void)
{
	int	i;

	if (clr_eol)
		putpad(clr_eol, 1);
	else {
		i = ncol - ttcol;
		while (i--)
			ttputc(' ');
		ttrow = ttcol = HUGE;
	}
}

/*
 * Erase to end of page.
 */
void
tteeop(void)
{
	int	line;

	if (clr_eos)
		putpad(clr_eos, nrow - ttrow);
	else {
		putpad(clr_eol, 1);
		if (insdel)
			ttdell(ttrow + 1, lines, lines - ttrow - 1);
		else {
			/* do it by hand */
			for (line = ttrow + 1; line <= lines; ++line) {
				ttmove(line, 0);
				tteeol();
			}
		}
		ttrow = ttcol = HUGE;
	}
}

/*
 * Make a noise.
 */
void
ttbeep(void)
{
	putpad(bell, 1);
	ttflush();
}

/*
 * Insert nchunk blank line(s) onto the screen, scrolling the last line on
 * the screen off the bottom.  Use the scrolling region if possible for a
 * smoother display.  If there is no scrolling region, use a set of insert
 * and delete line sequences.
 */
void
ttinsl(int row, int bot, int nchunk)
{
	int	i, nl;

	/* One line special cases */
	if (row == bot) {
		ttmove(row, 0);
		tteeol();
		return;
	}
	/* Use scroll region and back index */
	if (change_scroll_region && scroll_reverse) {
		nl = bot - row;
		ttwindow(row, bot);
		ttmove(row, 0);
		while (nchunk--)
			putpad(scroll_reverse, nl);
		ttnowindow();
		return;
	/* else use insert/delete line */
	} else if (insdel) {
		ttmove(1 + bot - nchunk, 0);
		nl = nrow - ttrow;
		if (parm_delete_line)
			putpad(tgoto(parm_delete_line, 0, nchunk), nl);
		else
			/* For all lines in the chunk */
			for (i = 0; i < nchunk; i++)
				putpad(delete_line, nl);
		ttmove(row, 0);

		/* ttmove() changes ttrow */
		nl = nrow - ttrow;

		if (parm_insert_line)
			putpad(tgoto(parm_insert_line, 0, nchunk), nl);
		else
			/* For all lines in the chunk */
			for (i = 0; i < nchunk; i++)
				putpad(insert_line, nl);
		ttrow = HUGE;
		ttcol = HUGE;
	} else
		panic("ttinsl: Can't insert/delete line");
}

/*
 * Delete nchunk line(s) from "row", replacing the bottom line on the
 * screen with a blank line.  Unless we're using the scrolling region,
 * this is done with crafty sequences of insert and delete lines.  The
 * presence of the echo area makes a boundary condition go away.
 */
void
ttdell(int row, int bot, int nchunk)
{
	int	i, nl;

	/* One line special cases */
	if (row == bot) {
		ttmove(row, 0);
		tteeol();
		return;
	}
	/* scrolling region */
	if (change_scroll_region) {
		nl = bot - row;
		ttwindow(row, bot);
		ttmove(bot, 0);
		while (nchunk--)
			putpad(scroll_fwd, nl);
		ttnowindow();
	/* else use insert/delete line */
	} else if (insdel) {
		ttmove(row, 0);
		nl = nrow - ttrow;
		if (parm_delete_line)
			putpad(tgoto(parm_delete_line, 0, nchunk), nl);
		else
			/* For all lines in the chunk */
			for (i = 0; i < nchunk; i++)
				putpad(delete_line, nl);
		ttmove(1 + bot - nchunk, 0);

		/* ttmove() changes ttrow */
		nl = nrow - ttrow;

		if (parm_insert_line)
			putpad(tgoto(parm_insert_line, 0, nchunk), nl);
		else
			/* For all lines in the chunk */
			for (i = 0; i < nchunk; i++)
				putpad(insert_line, nl);
		ttrow = HUGE;
		ttcol = HUGE;
	} else
		panic("ttdell: Can't insert/delete line");
}

/*
 * This routine sets the scrolling window on the display to go from line
 * "top" to line "bot" (origin 0, inclusive).  The caller checks for the
 * pathological 1-line scroll window which doesn't work right and avoids
 * it.  The "ttrow" and "ttcol" variables are set to a crazy value to
 * ensure that the next call to "ttmove" does not turn into a no-op (the
 * window adjustment moves the cursor).
 */
void
ttwindow(int top, int bot)
{
	if (change_scroll_region && (tttop != top || ttbot != bot)) {
		putpad(tgoto(change_scroll_region, bot, top), nrow - ttrow);
		ttrow = HUGE;	/* Unknown.		 */
		ttcol = HUGE;
		tttop = top;	/* Remember region.	 */
		ttbot = bot;
	}
}

/*
 * Switch to full screen scroll. This is used by "spawn.c" just before it
 * suspends the editor and by "display.c" when it is getting ready to
 * exit.  This function does a full screen scroll by telling the terminal
 * to set a scrolling region that is lines or nrow rows high, whichever is
 * larger.  This behavior seems to work right on systems where you can set
 * your terminal size.
 */
void
ttnowindow(void)
{
	if (change_scroll_region) {
		putpad(tgoto(change_scroll_region,
		    (nrow > lines ? nrow : lines) - 1, 0), nrow - ttrow);
		ttrow = HUGE;	/* Unknown.		 */
		ttcol = HUGE;
		tttop = HUGE;	/* No scroll region.	 */
		ttbot = HUGE;
	}
}

/*
 * Set the current writing color to the specified color. Watch for color
 * changes that are not going to do anything (the color is already right)
 * and don't send anything to the display.  The rainbow version does this
 * in putline.s on a line by line basis, so don't bother sending out the
 * color shift.
 */
void
ttcolor(int color)
{
	if (color != tthue) {
		if (color == CTEXT)
			/* normal video */
			putpad(exit_standout_mode, 1);
		else if (color == CMODE)
			/* reverse video */
			putpad(enter_standout_mode, 1);
		/* save the color */
		tthue = color;
	}
}

/*
 * This routine is called by the "refresh the screen" command to try
 * to resize the display. Look in "window.c" to see how
 * the caller deals with a change.
 *
 * We use `newrow' and `newcol' so vtresize() know the difference between the
 * new and old settings.
 */
void
ttresize(void)
{
	int newrow = 0, newcol = 0;

	struct	winsize winsize;

	if (ioctl(0, TIOCGWINSZ, &winsize) == 0) {
		newrow = winsize.ws_row;
		newcol = winsize.ws_col;
	}
	if ((newrow <= 0 || newcol <= 0) &&
	    ((newrow = lines) <= 0 || (newcol = columns) <= 0)) {
		newrow = 24;
		newcol = 80;
	}
	if (vtresize(1, newrow, newcol) != TRUE)
		panic("vtresize failed");
}

/*
 * fake char output for charcost()
 */
/* ARGSUSED */
static int
fakec(int c)
{
	cci++;
	return (0);
}

/* calculate the cost of doing string s */
static int
charcost(const char *s)
{
	cci = 0;

	tputs(s, nrow, fakec);
	return (cci);
}
