/*Similar to less(1), but handles terminal control sequences. When
  scrolling back, it redraws everything, which is ineffcient, but
  apparently the only thing possible without analyzing all control
  sequences.

  Compilation:
  gcc -O2 -o tless tless.c -lcurses

  Usage:
  tless [filename]

  Keys:
  j
  <ENTER>   one line down
  k         one line up
  n         half a screen down
  m         half a screen up
  <SPACE>   one screen down
  b         one screen up
  q, <ESC>  quit

  The notion of lines and screens is not very precise due to control
  sequences.

  Copyright (C) 2000 Frank Heckenbach <frank@g-n-u.de>

  This program is free software; you can redistribute it and/or
  modify it under the terms of the GNU General Public License as
  published by the Free Software Foundation, version 2.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program; see the file COPYING. If not, write to
  the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
  Boston, MA 02111-1307, USA. */

#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <fcntl.h>
#include <termios.h>
#include <term.h>
#include <curses.h>
#include <sys/types.h>

static int count = 0, size = 0, eof = 0, lastpos = -1, pos = 0, input, done = 0, tty;
static char *buf = NULL, *progname;
static struct termios old_termios;

static int putch (int c)
{
  return putchar (c);
}

static void out ()
{
  if (lastpos < 0 || lastpos > pos)
    {
      tputs (clear_screen, LINES, putch);
      fflush (stdout);
      lastpos = 0;
    }
  write (1, buf + lastpos, pos - lastpos);
  lastpos = pos;
}

static void down (int lc)
{
  int endpos = pos + lc * COLS / 4, bread;
  do
    {
      while (pos < endpos && pos < count && lc > 0)
        if (buf[pos++] == '\n')
          lc--;
      out ();
      if (pos < count || eof)
        break;
      if (count >= size && !(buf = realloc (buf, size += 0x10000)))
        {
          fprintf (stderr, "%s: out of memory", progname);
          done = 1;
          return;
        }
      bread = read (input, buf + count, size - count);
      if (bread < 0)
        {
          perror (progname);
          done = 1;
          return;
        }
      count += bread;
      if (bread == 0)
        eof = 1;
    }
  while (1);
}

static void up (int lc)
{
  int endpos;
  endpos = pos - lc * COLS / 4;
  if (endpos < 0)
    endpos = 0;
  while (pos > endpos && lc > 0)
    if (buf[--pos] == '\n')
      lc--;
  out ();
}

static void restore ()
{
  tcsetattr (tty, TCSANOW, &old_termios);
}

static void handler (int sig)
{
  restore ();
  signal (sig, SIG_DFL);
  raise (sig);
}

int main (int argc, char **argv)
{
  int bread, done = 0;
  char b[10];
  struct termios new_termios;
  progname = argv[0];
  if (argc < 2 || !strcmp (argv[1], "-"))
    input = 0;
  else
    input = open (argv[1], O_RDONLY);
  tty = open ("/dev/tty", O_RDONLY);
  if (tty < 0 || input < 0)
    {
      perror (progname);
      return 1;
    }
  setupterm (NULL, 1, NULL);
  tcgetattr (tty, &old_termios);
  new_termios = old_termios;
  new_termios.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON);
  new_termios.c_lflag &= ~(ECHO | ECHONL | ICANON | IEXTEN);
  new_termios.c_cflag &= ~(CSIZE | PARENB);
  new_termios.c_cflag |= CS8;
  new_termios.c_cc[VMIN] = 1;
  new_termios.c_cc[VTIME] = 0;
  tcsetattr (tty, TCSANOW, &new_termios);
  signal (SIGINT,  handler);
  signal (SIGTERM, handler);
  signal (SIGHUP,  handler);
  down (LINES - 1);
  while (!done)
    {
      bread = read (tty, b, sizeof (b));
      if (bread < 0)
        {
          perror (progname);
          break;
        }
      if (bread)
        switch (b[0])
          {
            case 27 : if (bread > 1) break;
                      /* FALLTHROUGH */
            case 'q':
            case 'Q': puts ("");
                      done = 1;
                      break;
            case 10:
            case 13:
            case 'j': down (1);
                      break;
            case 'k': up (1);
                      break;
            case 'n': down (LINES / 2 - 1);
                      break;
            case 'm': up (LINES / 2 - 1);
                      break;
            case ' ': down (LINES - 1);
                      break;
            case 'b': up (LINES - 1);
                      break;
          }
    }
  restore ();
  return 0;
}
