root/usr.bin/mg/tty.c
/*      $OpenBSD: tty.c,v 1.40 2023/03/08 04:43:11 guenther 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;

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()
 */
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);
}