/* vim:set ts=4 sw=4 et:
 *
 * main.c * xrootconsole
 * Original version: Copyright (C) 1998, 1999  Eric Youngblut
 * Current version:  Copyright (C) 1999        Eric Youngblut & Bob Galloway
 *
 * (The transparency stuff was inspired by -- pulled from the still-warm 
 * body of -- wterm.  Many thanks to wterm's "anonymous coder!")
 * 
 * $Id: main.c,v 1.13 2004/02/20 22:31:53 bob Exp $
 *
 *
 *
 * 
 **************************************************************************
 * 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; either version 2 of the License, or
 * (at your option) any later version.
 *
 * 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; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 * The author of the original version of this software can be reached
 * by e-mail at yngblut@cs.washington.edu.  The original version of
 * this software can be found at http://www.cs.washington.edu/homes/yngblut/
 *
 * The author of the current version of this software can be reached
 * by e-mail at bgallowa@wso.williams.edu.  The latest version of this
 * software can be found at http://de-fac.to/bob/xrootconsole/
 **************************************************************************/


#include "util.h"
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/Xos.h>
#include <X11/Xatom.h>
#include <assert.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>

#ifndef XROOTCONSOLE_VERSION
#define XROOTCONSOLE_VERSION "undefined"
#endif

#define USAGE \
"Usage: xrootconsole [options] [console]\n" \
"Scroll the console to a transparent window placed on the X root window.\n" \
"\n" \
"  -geometry GEO    the geometry of the window (default 80x10+0+0)\n" \
"  -fn FONT         the font to use (default fixed)\n" \
"  -fg COLOR        foreground color of the text (default white)\n" \
"  -bg COLOR        background AND-mask for shaded transparency (default clear)\n" \
"  -bd COLOR        border color (default white)\n" \
"  -bw WIDTH        border width (default 0)\n" \
"  -c COLUMNS       split window into number of text columns (default 1)\n" \
"  --solid          make background a solid color, not shaded-transparent\n" \
"  --topdown        insert lines at the top and scroll the rest down\n" \
"  --wrap           wrap long lines, instead of cutting them off\n" \
"  -h or --help     a familiar-looking help screen\n" \
"  -v or --version  displays the version number\n" \
"  [console]        filename to read (defaults to standard input)\n" \
"\n" \
"Report bugs to <bob@de-fac.to>.\n"


#define DEFAULT_X        0
#define DEFAULT_Y        0
#define DEFAULT_COLS        80
#define DEFAULT_ROWS        10
#define DEFAULT_FONT_NAME         "fixed"
#define DEFAULT_FOREGROUND_COLOR  "white"
#define DEFAULT_BACKGROUND_COLOR  "white"
#define DEFAULT_BORDER_COLOR      "white"
#define DEFAULT_BORDER_WIDTH      0
#define DEFAULT_TEXT_COLUMNS      1
#define DEFAULT_WRAP     False
#define DEFAULT_TOPDOWN  False
#define DEFAULT_SOLID    False


typedef struct {
    const char* fg;
    const char* bg;
    const char* bd;
    const char* geometry;
    const char* console_name;
    const char* font_name; 
    int bw;
    int tc;               /* number of text columns (NOT characters) */
    int wrap;
    int topdown;
    int solid;
} InitOptions;

typedef struct {
    int x_root;           /* position of the window (in pixels) */
    int y_root;
    int width;            /* size of the window (in pixels) */ 
    int height;
    unsigned int cols;             /* size of the window (in characters) */
    unsigned int rows;
    int geometry;
    Display* dpy;
    Window win;
    Pixmap buf;  /* the main drawable, copied to win by exposures */
    Pixmap bkg;  /* the background pixmap, copied to buf before each drawing */
    XFontStruct* fn;      /* text font */
    GC foregc; 
    GC backgc;
    int colwidth;         /* width of a column in characters */
    int nlines;           /* number of lines to keep in memory */
    int intercolgap;      /* width between two columns */
    XSetWindowAttributes swa;
} WindowSettings;

typedef struct {
    const char* console;   /* name of the console file */
    int fd;                /* file descriptor of the console file */
    char* text;            /* circular buffer to store the characters */
    int pos;               /* where the actual first line in text is */
} ConsoleSettings;


/*
 * put
 */

void put(const char* s, InitOptions *io, 
         WindowSettings *ws, ConsoleSettings *cs) {
    static int x = 0, y = 0;
  
    while (*s != '\0') {
        switch (*s) {
        case '\n':
            x = 0;
            if (io->topdown) {
                cs->pos = (cs->pos + ws->nlines - 1) % ws->nlines;
                memset(cs->text + cs->pos * ws->colwidth, ' ', ws->colwidth);
            } 
            else if (y == ws->nlines - 1) {
                memset(cs->text + cs->pos * ws->colwidth, ' ', ws->colwidth);
                cs->pos = (cs->pos + 1) % ws->nlines;
            }
            else y++;
            break;
      
        case '\t':
            x = (x / 8 + 1) * 8;
            if (x >= ws->cols && io->wrap) put("\n",io,ws,cs);
            break;
      
        default:
            if (*s >= ' ' && x < ws->colwidth) {
                cs->text[x + ((y + cs->pos) % ws->nlines) * ws->colwidth] = *s;
                if (++x == ws->colwidth && io->wrap) put("\n",io,ws,cs);
            }
            break;
        }
    
        s++;
    }
}


/*
 * write the text in the pixmap
 */
void draw_pixmap(InitOptions *io, WindowSettings *ws, ConsoleSettings *cs) {
    int rowc = cs->pos + (io->topdown==True);    /* row in caracters */
    int rowp = ws->fn->ascent;        /* row in pixels */
    int i,j;

    /* setup the background */
    XCopyArea(ws->dpy, ws->bkg, ws->buf, ws->foregc, 0, 0, 
              ws->width, ws->height, 0, 0);
    
    /* write the string */
    for (i = ws->rows; i>0; --i) {
        for (j = 0; j < io->tc; ++j) {
            XDrawString(ws->dpy, ws->buf, ws->foregc, 
                j * (ws->colwidth * ws->fn->max_bounds.width + ws->intercolgap),
                rowp, 
                cs->text + ((rowc + j*(ws->rows)) % ws->nlines)*(ws->colwidth), 
                ws->colwidth);   
        }
        rowp += ws->fn->ascent + ws->fn->descent;
        ++rowc;
    }  
}


/*
 * init
 */


void init_options(char **argv, InitOptions *io) {
    /* initialize InitOptions struct*/
    io->console_name = NULL;
    io->geometry = NULL;
    io->fg = DEFAULT_FOREGROUND_COLOR;
    io->bg = DEFAULT_BACKGROUND_COLOR;
    io->bd = DEFAULT_BORDER_COLOR;
    io->bw = DEFAULT_BORDER_WIDTH;
    io->tc = DEFAULT_TEXT_COLUMNS;
    io->font_name = DEFAULT_FONT_NAME;
    io->wrap = DEFAULT_WRAP;
    io->topdown = DEFAULT_TOPDOWN;
    io->solid = DEFAULT_SOLID;
     
    /* Process command-line arguments */
    while (*++argv != NULL) {
        if (!strcmp(*argv, "-geometry") || !strcmp(*argv, "-geo")) 
            io->geometry = *++argv;
        else if (!strcmp(*argv, "-font") || !strcmp(*argv, "-fn"))
            io->font_name = *++argv;
        else if (!strcmp(*argv, "-foreground") || !strcmp(*argv, "-fg"))
            io->fg = *++argv;
        else if (!strcmp(*argv, "-background") || !strcmp(*argv, "-bg"))
            io->bg = *++argv;
        else if (!strcmp(*argv, "-bordercolor") || !strcmp(*argv, "-bd"))
            io->bd = *++argv;
        else if (!strcmp(*argv, "-borderwidth") || !strcmp(*argv, "-bw"))
            io->bw = atoi(*++argv);
        else if (!strcmp(*argv, "-columns") || !strcmp(*argv, "-c"))
            io->tc = atoi(*++argv);
        else if (!strcmp(*argv, "--solid"))
            io->solid = True;
        else if (!strcmp(*argv, "--wrap"))
            io->wrap = True;
        else if (!strcmp(*argv, "--topdown"))
            io->topdown = True;
        else if (!strcmp(*argv, "--version") || !strcmp(*argv, "-v")) {
            fprintf(stderr, "xrootconsole %s\n", XROOTCONSOLE_VERSION);
            exit(EXIT_SUCCESS);
        }
        else if (!strcmp(*argv, "--help") || !strcmp(*argv, "-h")) {
            fprintf(stderr, "xrootconsole %s\n%s", XROOTCONSOLE_VERSION,USAGE);
            exit(EXIT_SUCCESS);
        }
        else {
            io->console_name = *argv;
        }
        /* sanity check! */
        if (io->tc < 1) io->tc = DEFAULT_TEXT_COLUMNS;
    }
}    

int open_console(const char* console) {
    int fd;
    assert(console!=NULL);
    fd = open(console, O_RDONLY|O_NONBLOCK);  
    if (fd < 0) {
        fprintf(stderr,"Console %s can't be opened!  ",console);
        perror("Error");
        exit (EXIT_FAILURE);
    }
    return fd;
}

void reset_console(ConsoleSettings *cs) {
    assert(cs->fd >= 0);
    assert((cs->fd >= 1) || (cs->console == NULL));
    if (cs->console == NULL) exit(EXIT_SUCCESS);
    close(cs->fd);
    cs->fd = -1;
}

void init_console(const char *console, 
                    int colwidth, int nlines, 
                    ConsoleSettings *cs) {

    /* we never modify the name through a ConsoleSettings*, 
       so don't bother copying the string */
    cs->console = console;

    /* Create the text array. We can display "nlines-1" # of rows, and the 
       extra one is for the line being read. */
    cs->text = (char*)malloc(colwidth * nlines);
    memset(cs->text, ' ', colwidth * nlines);
    cs->pos = 0;
    if (cs->console == NULL) cs->fd = 0;      /* STDIN special case, */
    else cs->fd = open_console(cs->console);  /* or open normal file */
}
  

/* Return the root window, or a virtual root window if any. */
static Window root_window(Display *display) {
    Atom __SWM_VROOT = XInternAtom(display, "__SWM_VROOT", False);

    if (__SWM_VROOT != None) {
        Window unused, *windows;
        unsigned int count;

        if (XQueryTree(display, DefaultRootWindow (display), &unused, &unused,
		      &windows, &count)) {
            int i;

            for (i=0; i<count; i++) {
                Atom type;
                int format;
                unsigned long nitems, bytes_after_return;
                Window *virtual_root_window;

                if (XGetWindowProperty(display, windows[i], __SWM_VROOT,
                          0, 1, False, XA_WINDOW, &type, &format,
                          &nitems, &bytes_after_return,
                          (unsigned char **) &virtual_root_window) == Success){
                    if (type != None) {
                        if (type == XA_WINDOW) {
                            XFree(windows);
                            return *virtual_root_window;
                        } else {
                            fprintf(stderr,
                                    "__SWM_VROOT property type mismatch");
                        }
                    }
                } else {
                    fprintf(stderr,
                         "failed to get __SWM_VROOT property on window 0x%lx",
                         windows[i]);
                }
            }

        if (count)
            XFree(windows);
        } else {
            fprintf(stderr, "Can't query tree on root window 0x%lx",
                 DefaultRootWindow (display));
        }
    } else {
        /* This shouldn't happen. The Xlib documentation is wrong BTW. */
        fprintf (stderr, "Can't intern atom __SWM_VROOT");
    }

    return DefaultRootWindow (display);
}


void init_window(InitOptions *io, WindowSettings *ws) {
    XGCValues values;

    memset(ws,0,sizeof(*ws));
    ws->x_root = DEFAULT_X;
    ws->y_root = DEFAULT_Y;
    ws->cols   = DEFAULT_COLS;
    ws->rows   = DEFAULT_ROWS;
    ws->intercolgap = 0;

    if (io->geometry) {
        ws->geometry = XParseGeometry(io->geometry, 
                &(ws->x_root), &(ws->y_root), &(ws->cols), &(ws->rows));
    }

    /* Connect to the X server */
    ws->dpy = XOpenDisplay(NULL);
    if (ws->dpy == NULL) {
        fprintf(stderr, "Cannot open display\n");
        exit(EXIT_FAILURE);
    }

    ws->fn = load_font(io->font_name, ws->dpy);

    /* compute text column geometry */
    ws->nlines = ws->rows * io->tc + 1;
    ws->colwidth = ws->cols / io->tc;
    if (io->tc > 1) {
        /* ensure at least a 1 char gap between columns */
        if ((ws->cols - ws->colwidth*io->tc)/(io->tc-1) < 1) --(ws->colwidth); 
        ws->intercolgap = (ws->cols - ws->colwidth*io->tc) * 
                            ws->fn->max_bounds.width / (io->tc-1);
    }

    /* Calculate the position of window on the screen */
    ws->width = ws->cols * ws->fn->max_bounds.width;
    ws->height = ws->rows * (ws->fn->ascent + ws->fn->descent);
    ws->x_root = (ws->geometry & XNegative) ?
        (DisplayWidth(ws->dpy, DefaultScreen(ws->dpy)) - 
            ws->width + ws->x_root - (2*io->bw)) : 
        ws->x_root;
    ws->y_root = (ws->geometry & YNegative) ?
        (DisplayHeight(ws->dpy, DefaultScreen(ws->dpy)) - 
            ws->height + ws->y_root - (2*io->bw)) : 
        ws->y_root;


    ws->swa.background_pixmap = ParentRelative;
    ws->swa.border_pixel = load_color(io->bd, ws->dpy);
    ws->swa.event_mask = ExposureMask;
    ws->swa.override_redirect = True;
     
    ws->win = XCreateWindow(ws->dpy, root_window(ws->dpy), 
                ws->x_root, ws->y_root,
                ws->width, ws->height, io->bw, CopyFromParent, CopyFromParent,
                DefaultVisual(ws->dpy, DefaultScreen(ws->dpy)),
                CWOverrideRedirect | CWBackPixmap | CWEventMask |
                CWBorderPixel, &(ws->swa));
    
    XMapWindow(ws->dpy, ws->win);

    /* Create the pixmaps */
    ws->bkg = XCreatePixmap(ws->dpy, ws->win, ws->width, ws->height,
            DefaultDepth(ws->dpy, DefaultScreen(ws->dpy)));
    ws->buf = XCreatePixmap(ws->dpy, ws->win, ws->width, ws->height,
            DefaultDepth(ws->dpy, DefaultScreen(ws->dpy)));

    /* Create the foreground GC */
    values.font = ws->fn->fid;
    values.foreground = load_color(io->fg, ws->dpy);
    values.graphics_exposures = 0;
    ws->foregc = XCreateGC(ws->dpy, ws->win, 
           GCFont | GCForeground | GCGraphicsExposures, 
           &values);
     
    /* initialize the background pixmap */
    values.background = values.foreground;
    values.foreground = load_color(io->bg, ws->dpy);
    if (io->solid == True) {

        XLowerWindow(ws->dpy, ws->win);
        ws->backgc = XCreateGC(ws->dpy, ws->win, 
                        GCForeground | GCBackground, &values);
        XFillRectangle(ws->dpy, ws->bkg, ws->backgc, 0, 0, 
                        ws->width, ws->height);

    } else { /* shaded/transparent background */
        
        values.function = GXand;
        ws->backgc = XCreateGC(ws->dpy, ws->win, 
               GCForeground | GCBackground | GCFunction,
               &values);

        /* fill the window with root pixmap */
        XClearWindow(ws->dpy,ws->win);

        /* take a snapshot for the root pixmap */
        XCopyArea(ws->dpy, ws->win, ws->bkg, ws->foregc, 0, 0, 
                    ws->width, ws->height, 0, 0);     

        /* in order to get the full pixmap we have to wait until the pixmap
           is copied before lowering the window */
        XLowerWindow(ws->dpy, ws->win);

        /* AND-shade the background */
        XFillRectangle(ws->dpy, ws->bkg, ws->backgc, 0, 0,
                        ws->width, ws->height);
    }

    /* prevent the server from redrawing the background */
    ws->swa.background_pixmap = None;
    XChangeWindowAttributes(ws->dpy, ws->win, CWBackPixmap, &(ws->swa));
}


/*
 * handle_expose
 */

void handle_expose(XExposeEvent* ev, WindowSettings *ws) {
    XCopyArea(ws->dpy, ws->buf, ws->win, ws->foregc,
          ev->x, ev->y, ev->width, ev->height,
          ev->x, ev->y );
}





/*
 * event_loop
 */

void event_loop(InitOptions *io, WindowSettings *ws, ConsoleSettings *cs) {
    fd_set rfds;
    fd_set efds;
    int Xfd = ConnectionNumber(ws->dpy);
    int maxfd;
    struct timeval select_timeout;
    XEvent ev;
  
    maxfd = (cs->fd > Xfd ? cs->fd : Xfd) + 1;

    while (1) {

        while ((XPending(ws->dpy) > 0) && 
            (XCheckMaskEvent(ws->dpy, ExposureMask, &ev) == True)) { 
            handle_expose((XExposeEvent*)&ev.xexpose, ws);
        }

        XLowerWindow(ws->dpy, ws->win);

        /* has the file moved from under us?  re-open if so. */
        if (cs->fd < 0) {
            cs->fd = open_console(cs->console);
            maxfd = (cs->fd > Xfd ? cs->fd : Xfd) + 1;
        }

        FD_ZERO(&rfds); 
        FD_ZERO(&efds);
        FD_SET(Xfd, &rfds);
        FD_SET(cs->fd, &rfds);
        FD_SET(cs->fd, &efds);
        select_timeout.tv_sec  = 5;
        select_timeout.tv_usec = 0;
        if (select(maxfd, &rfds, NULL, &efds, &select_timeout) == -1) {
            perror("select error");
            exit(EXIT_FAILURE);
        }
    
        if (FD_ISSET(cs->fd, &rfds)) {
            char buf[1024];
            int n;

            n = read(cs->fd, buf, sizeof(buf) - 1);
            if (n == -1) {
                sleep(1);
                if (errno != EAGAIN) {reset_console(cs);}
            } else if (n > 0) {
                buf[n] = '\0';
                put(buf,io,ws,cs);
                draw_pixmap(io,ws,cs);
                /* make an exposure event */
                XClearArea(ws->dpy, ws->win, 0, 0, 0, 0, True);  
            } else {  /* n == 0  --> EOF */
                sleep(1); 
            }
        }

        else if (FD_ISSET(cs->fd, &efds)) {
            reset_console(cs);
            sleep(1);
        }
        
    } /* end loop */
}

/*
 * main
 */

int main(int argc, char** argv) {
    InitOptions io;
    ConsoleSettings cs;
    WindowSettings ws;
  
    init_options(argv, &io);
    init_window(&io,&ws);
    init_console(io.console_name, ws.colwidth, ws.nlines, &cs);

    /* Display a message */
    put("xrootconsole ",&io,&ws,&cs);
    put(XROOTCONSOLE_VERSION,&io,&ws,&cs);
    put("\n",&io,&ws,&cs);
    
    draw_pixmap(&io,&ws,&cs);

    event_loop(&io,&ws,&cs);
  
    XCloseDisplay(ws.dpy);
    exit(EXIT_SUCCESS);
}
