/*
 * resizecons.c - change console video mode
 *
 * Version 1.00
 *
 * How to use this:
 *
 * 1. Get svgalib, make restoretextmode, put it somewhere in your path.
 * 2. Put vga=ask in /etc/lilo/config, boot your machine a number of times,
 *    each time with a different vga mode, and run the command
 *        "restoretextmode -w COLSxROWS".
 *    For me this resulted in the files 80x25, 80x28, 80x50, 80x60, 100x40,
 *    132x25, 132x28, 132x44. Put these files in /usr/lib/kbd/videomodes
 *    (or in your current dir).
 * 3. Now "resizecons COLSxROWS" will change your video mode. (Assuming you
 *    have an appropriate kernel, and svgalib works for your video card.)
 *
 * Note: this is experimental, but it works for me. Comments are welcome.
 * You may have to be root to get the appropriate ioperm permissions.
 * It is not safe to make this program suid root.
 *
 * aeb@cwi.nl - 940924
 *
 * Harm Hanemaaijer added the -lines option, which reprograms the
 * number of scanlines. He writes:
 *
 * Added -lines option, which reprograms the number of scanlines and
 * the font height of the VGA hardware with register I/O, so that
 * switching is possible between textmodes with different numbers
 * of lines, in a VGA compatible way. It should work for 132 column
 * modes also, except that number of columns cannot be changed.
 *
 * Standard VGA textmode uses a 400 scanline screen which is refreshed
 * at 70 Hz. The following modes are supported that use this vertical
 * resolution (C is the number of columns, usually 80 or 132).
 *
 *	mode		font height
 *	C x 25		16
 *	C x 28		14
 *	C x 36		11	(non-standard height)
 *	C x 44		9	(8-line fonts are a good match)
 *	C x 50		8
 *
 * The following modes are supported with a 480 scanline resolution,
 * refresh at 60 Hz. Some not quite VGA compatible displays may not
 * support this (it uses the same vertical timing as standard VGA
 * 640x480x16 graphics mode).
 *
 *	mode		font height
 *	C x 30		16
 *	C x 34		14
 *	C x 40		12	(non-standard height)
 *	C x 60		8
 *
 * Two 12-line fonts are already in the consolefonts directory,
 * namely lat1-12.psfu.gz and lat2-12.psfu.gz.
 * For the 36 lines mode (11 line font), lat1-10.psfu.gz and lat2-10.psfu.gz
 * can be used.
 * 
 * hhanemaa@cs.ruu.nl - 941028
 * 
 * Notes:
 *
 * In the consolefonts directory there is 'default8x9' font file but
 * no 'default8x8'. Why is this? The standard VGA BIOS has an 8-line
 * font, and they are much more common in SVGA modes (e.g. 50 and 60
 * row modes). It is true that standard VGA textmode uses effectively
 * 9 pixel wide characters, but that has nothing to do with the font
 * data.
 */

#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
#include <signal.h>
#include <sys/ioctl.h>
#if (__GNU_LIBRARY__ >= 6)
#include <sys/perm.h>
#else
#include <linux/types.h>
#include <linux/termios.h>
#endif
#include <linux/vt.h>
#include "paths.h"
#include "getfd.h"
#include "findfile.h"
#include "nls.h"
#include "version.h"

#define MODE_RESTORETEXTMODE	0
#define MODE_VGALINES		1

static void usage(void);

/* VGA textmode register tweaking. */
static void vga_init_io(void);
static void vga_400_scanlines(void);
static void vga_480_scanlines(void);
static void vga_set_fontheight(int);
static int vga_get_fontheight(void);
static void vga_set_cursor(int, int);
static void vga_set_verticaldisplayend_lowbyte(int);

char *dirpath[] = { "", DATADIR "/" VIDEOMODEDIR "/", 0};
char *suffixes[] = { "", 0 };

int
main(int argc, char **argv) {
    int rr, cc, fd, i, mode;
    struct vt_sizes vtsizes;
    struct vt_stat vtstat;
    struct winsize winsize;
    char *p;
    char tty[12], cmd[80], infile[1024];
    FILE *fin;
    char *defaultfont;

    set_progname(argv[0]);

    setlocale(LC_ALL, "");
    bindtextdomain(PACKAGE_NAME, LOCALEDIR);
    textdomain(PACKAGE_NAME);

    if (argc < 2)
      usage();

    if (argc == 2 && !strcmp(argv[1], "-V"))
      print_version_and_exit();

    rr = 0;			/* make gcc happy */
    cc = atoi(argv[1]);
    mode = MODE_RESTORETEXTMODE;
    if (argc == 3 && strcmp(argv[1], "-lines") == 0) {
    	mode = MODE_VGALINES;
    	rr = atoi(argv[2]);
    }
    else
    if (argc == 2 && (p = index(argv[1], 'x')) != 0)
      rr = atoi(p+1);
    else if(argc == 3)
      rr = atoi(argv[2]);
    else
      usage();

    if (mode == MODE_RESTORETEXTMODE) {
        /* prepare for: restoretextmode -r 80x25 */
        sprintf(infile, "%dx%d", cc, rr);
        fin = findfile(infile, dirpath, suffixes);
        if (!fin) {
	    fprintf(stderr, _("resizecons: cannot find videomode file %s\n"),
		    infile);
	    exit(1);
	}
 	fpclose(fin);
    }

    fd = getfd(NULL);

    if(ioctl(fd, TIOCGWINSZ, &winsize)) {
	perror("TIOCGWINSZ");
	exit(1);
    }

    if (mode == MODE_VGALINES) {
    	/* Get the number of columns. */
    	cc = winsize.ws_col;
    	if (rr != 25 && rr != 28 && rr !=30 && rr != 34 && rr != 36
    	&& rr != 40 && rr != 44 && rr != 50 && rr != 60) {
    	    fprintf(stderr, _("Invalid number of lines\n"));
	    exit(1);
	}
    }

    if(ioctl(fd, VT_GETSTATE, &vtstat)) {
	perror("VT_GETSTATE");
	exit(1);
    }

    vtsizes.v_rows = rr;
    vtsizes.v_cols = cc;
    vtsizes.v_scrollsize = 0;

    vga_init_io();		/* maybe only if (mode == MODE_VGALINES) */

    if(ioctl(fd, VT_RESIZE, &vtsizes)) {
	perror("VT_RESIZE");
	exit(1);
    }

    if (mode == MODE_VGALINES) {
        /* Program the VGA registers. */
        int scanlines_old;
        int scanlines_new;
        int fontheight;
   	if (winsize.ws_row == 25 || winsize.ws_row == 28 ||
   	winsize.ws_row == 36 || winsize.ws_row == 44 ||
   	winsize.ws_row == 50)
   	    scanlines_old = 400;
   	else
   	    scanlines_old = 480;
   	if (rr == 25 || rr == 28 || rr == 36 || rr == 44 || rr == 50)
   	    scanlines_new = 400;
   	else
   	    scanlines_new = 480;
        /* Switch to 400 or 480 scanline vertical timing if required. */
        if (scanlines_old != 400 && scanlines_new == 400)
            vga_400_scanlines();
        if (scanlines_old != 480 && scanlines_new == 480)
            vga_480_scanlines();
        switch (rr) {
        case 25 : fontheight = 16; break;
        case 28 : fontheight = 14; break;
        case 30 : fontheight = 16; break;
        case 34 : fontheight = 14; break;
        case 36 : fontheight = 12; break;
        case 40 : fontheight = 12; break;
        case 44 : fontheight = 9; break;
        case 50 : fontheight = 8; break;
        case 60 : fontheight = 8; break;
        default : fontheight = 8; break;
	}
	/* Set the VGA character height. */
	vga_set_fontheight(fontheight);
	/* Set the line offsets within a character cell of the cursor. */
	if (fontheight >= 10)
	    vga_set_cursor(fontheight - 3, fontheight - 2);
	else
	    vga_set_cursor(fontheight - 2, fontheight - 1);
	/*
	 * If there are a few unused scanlines at the bottom of the
	 * screen, make sure they are not displayed (otherwise
	 * there is a annoying changing partial line at the bottom).
	 */
        vga_set_verticaldisplayend_lowbyte((fontheight * rr - 1) & 0xff);
	printf(_("Old mode: %dx%d  New mode: %dx%d\n"), winsize.ws_col,
		winsize.ws_row, cc, rr);
	printf(_("Old #scanlines: %d  New #scanlines: %d  Character height: %d\n"),
		scanlines_old, scanlines_new, fontheight);
    }

    if (mode == MODE_RESTORETEXTMODE) {
	/* do: restoretextmode -r 25x80 */
	sprintf(cmd, "restoretextmode -r %s\n", pathname);
	errno = 0;
	if(system(cmd)) {
	    if(errno)
		perror("restoretextmode");
	    fprintf(stderr, _("resizecons: the command `%s' failed\n"), cmd);
	    exit(1);
	}
    }

    /*
     * for i in /dev/tty[0-9] /dev/tty[0-9][0-9]
     * do
     *     stty rows $rr cols $cc < $i
     * done
     * kill -SIGWINCH `cat /tmp/selection.pid`
     */
    winsize.ws_row = rr;
    winsize.ws_col = cc;
    for (i=0; i<16; i++)
      if (vtstat.v_state & (1<<i)) {
	  sprintf(tty, "/dev/tty%d", i);
	  fd = open(tty, O_RDONLY);
	  if (fd < 0 && errno == ENOENT) {
	      sprintf(tty, "/dev/vc/%d", i);
	      fd = open(tty, O_RDONLY);
	  }
	  if (fd >= 0) {
	      if(ioctl(fd, TIOCSWINSZ, &winsize))
		perror("TIOCSWINSZ");
	      close(fd);
	  }
      }

#if 0
    /* Try to tell selection about the change */
    /* [may be a security risk?] */
    if ((fd = open("/tmp/selection.pid", O_RDONLY)) >= 0) {
	char buf[64];
	int n = read(fd, buf, sizeof(buf));
	if (n > 0) {
	    int pid;

	    buf[n-1] = 0;
	    pid = atoi(buf);
	    kill(pid, SIGWINCH);
	}
	close(fd);
    }
#endif

    /* do: setfont default8x16 */
    /* (other people might wish other fonts - this should be settable) */

    /* We read the VGA font height register to be sure. */
    /* There isn't much consistency in this. */
    switch (vga_get_fontheight()) {
    case 8 :
    case 9 : defaultfont = "default8x9"; break;
    case 10 : defaultfont = "lat1-10"; break;
    case 11 :
    case 12 : defaultfont = "lat1-12"; break;
    case 13 :
    case 14 : defaultfont = "iso01.14"; break;
    case 15 :
    case 16 :
    default : defaultfont = "default8x16"; break;
    }

    sprintf(cmd, "setfont %s", defaultfont);
    errno = 0;
    if(system(cmd)) {
	if(errno)
	    perror("setfont");
	fprintf(stderr, "resizecons: the command `%s' failed\n", cmd);
	exit(1);
    }

    fprintf(stderr, _("resizecons: don't forget to change TERM "
		      "(maybe to con%dx%d or linux-%dx%d)\n"),
	    cc, rr, cc, rr);
    if (getenv("LINES") || getenv("COLUMNS"))
      fprintf(stderr,
	      "Also the variables LINES and COLUMNS may need adjusting.\n");

    return 0;
}

static void
usage() {
    fprintf(stderr,
	    _("resizecons:\n"
	      "call is:  resizecons COLSxROWS  or:  resizecons COLS ROWS\n"
	      "or: resizecons -lines ROWS, with ROWS one of 25, 28, 30, 34,"
	      " 36, 40, 44, 50, 60\n"));
    exit(1);
}

/*
 * The code below is used only with the option `-lines ROWS', and is
 * very hardware dependent, and requires root privileges.
 */

/* Port I/O macros. Note that these are not compatible with the ones */
/* defined in the kernel header files. */

static inline void outb( int port, int value )
{
	__asm__ volatile ("outb %0,%1"
	: : "a" ((unsigned char)value), "d" ((unsigned short)port));
}

static inline int inb( int port )
{
	unsigned char value;
	__asm__ volatile ("inb %1,%0"
		: "=a" (value)
		: "d" ((unsigned short)port));
	return value;
}


/* VGA textmode register tweaking functions. */

static int crtcport;

static void vga_init_io() {
	if (iopl(3) < 0) {
		fprintf(stderr,
			_("resizecons: cannot get I/O permissions.\n"));
		exit(1);
	}
	crtcport = 0x3d4;
	if ((inb(0x3cc) & 0x01) == 0)
		crtcport = 0x3b4;
}

static void vga_set_fontheight( int h ) {
	outb(crtcport, 0x09);
	outb(crtcport + 1, (inb(crtcport + 1) & 0xe0) | (h - 1));
}

static int vga_get_fontheight() {
	outb(crtcport, 0x09);
	return (inb(crtcport + 1) & 0x1f) + 1;
}

static void vga_set_cursor( int top, int bottom ) {
	outb(crtcport, 0x0a);
	outb(crtcport + 1, (inb(crtcport + 1) & 0xc0) | top);
	outb(crtcport, 0x0b);
	outb(crtcport + 1, (inb(crtcport + 1) & 0xe0) | bottom);
}

static void vga_set_verticaldisplayend_lowbyte( int byte ) {
	/* CRTC register 0x12 */
	/* vertical display end */
	outb(crtcport, 0x12);
	outb(crtcport + 1, byte);
}

static void vga_480_scanlines() {
	/* CRTC register 0x11 */
	/* vertical sync end (also unlocks CR0-7) */
	outb(crtcport, 0x11);
	outb(crtcport + 1, 0x0c);

	/* CRTC register 0x06 */
	/* vertical total */
	outb(crtcport, 0x06);
	outb(crtcport + 1, 0x0b);

	/* CRTC register 0x07 */
	/* (vertical) overflow */
	outb(crtcport, 0x07);
	outb(crtcport + 1, 0x3e);

	/* CRTC register 0x10 */
	/* vertical sync start */
	outb(crtcport, 0x10);
	outb(crtcport + 1, 0xea);

	/* CRTC register 0x12 */
	/* vertical display end */
	outb(crtcport, 0x12);
	outb(crtcport + 1, 0xdf);

	/* CRTC register 0x15 */
	/* vertical blank start */
	outb(crtcport, 0x15);
	outb(crtcport + 1, 0xe7);

	/* CRTC register 0x16 */
	/* vertical blank end */
	outb(crtcport, 0x16);
	outb(crtcport + 1, 0x04);

	/* Misc Output register */
	/* Preserver clock select bits and set correct sync polarity */
	outb(0x3c2, (inb(0x3cc) & 0x0d) | 0xe2);
}

static void vga_400_scanlines() {
	/* CRTC register 0x11 */
	/* vertical sync end (also unlocks CR0-7) */
	outb(crtcport, 0x11);
	outb(crtcport + 1, 0x0e);

	/* CRTC register 0x06 */
	/* vertical total */
	outb(crtcport, 0x06);
	outb(crtcport + 1, 0xbf);

	/* CRTC register 0x07 */
	/* (vertical) overflow */
	outb(crtcport, 0x07);
	outb(crtcport + 1, 0x1f);

	/* CRTC register 0x10 */
	/* vertical sync start */
	outb(crtcport, 0x10);
	outb(crtcport + 1, 0x9c);

	/* CRTC register 0x12 */
	/* vertical display end */
	outb(crtcport, 0x12);
	outb(crtcport + 1, 0x8f);

	/* CRTC register 0x15 */
	/* vertical blank start */
	outb(crtcport, 0x15);
	outb(crtcport + 1, 0x96);

	/* CRTC register 0x16 */
	/* vertical blank end */
	outb(crtcport, 0x16);
	outb(crtcport + 1, 0xb9);

	/* Misc Output register */
	/* Preserver clock select bits and set correct sync polarity */
	outb(0x3c2, (inb(0x3cc) & 0x0d) | 0x62);
}
