view nnchat.c @ 301:388c22d6ab44

Closing PRV buffers should not depend on the user being in the internal userlist. Fixed.
author Matti Hamalainen <ccr@tnsp.org>
date Sat, 11 Jun 2011 04:30:54 +0300
parents 0119bcc15f15
children 859578ec3275
line wrap: on
line source

/*
 * NNChat - Custom chat client for NewbieNudes.com chatrooms
 * Written by Matti 'ccr' Hämäläinen
 * (C) Copyright 2008-2011 Tecnic Software productions (TNSP)
 */
#include "libnnchat.h"
#include <stdlib.h>
#include "th_args.h"
#include "th_config.h"
#include <string.h>
#include <errno.h>
#ifdef __WIN32
/* Undefine because both windows.h and curses.h #define it */
#undef MOUSE_MOVED
#include <shlwapi.h>
#else
#include <sys/wait.h>
#endif
#ifdef HAVE_NCURSES_H
#include <ncurses.h>
#else
#include <curses.h>
#endif

#ifdef __WIN32
#define SET_CONFIG_FILE    "nnchat.txt"
#define SET_DIR_SEPARATOR  "\\"
#define SET_DELAY          (0)
#else
#define SET_CONFIG_FILE    ".nnchat"
#define SET_DIR_SEPARATOR  "/"
#define SET_DELAY          (5)
#endif

#define SET_BACKBUF_LEN (512)       /* Backbuffer size (in lines) */
#define SET_MAX_HISTORY (16)        /* Command history length */
#define SET_KEEPALIVE   (15*60)     /* Ping/keepalive period in seconds */
#define SET_MAX_WINDOWS (32)


typedef struct {
    qringbuf_t *data;   /* "Backbuffer" data for this window */
    int pos;            /* Current position in the window, 0 = real time */
    BOOL dirty;

    char *id;           /* Chatter ID, NULL = main window */
    int num;		/* Window number */
    
    char *buf;
    size_t len, bufsize;
} nn_window_t;


/* Options
 */
int     optPort = 8005;
int     optUserColor = 0x000000;
char    *optServer = "chat.newbienudes.com",
        *optUserName = NULL,
        *optUserNameCmd = NULL,
        *optUserNameEnc = NULL,
        *optPassword = NULL,
        *optPasswordCmd = NULL,
        *optLogFilename = NULL,
        *optSite = "NN";
char    optNickSep = ':';
BOOL    optDaemon = FALSE;
FILE    *optLogFile = NULL;
BOOL    setIgnoreMode = FALSE;
BOOL    optDebug = FALSE;
BOOL    optLogEnable = FALSE;

nn_window_t *chatWindows[SET_MAX_WINDOWS],
        *currWin = NULL;
WINDOW  *mainWin = NULL,
        *statusWin = NULL,
        *editWin = NULL;

qlist_t *setIgnoreList = NULL,
        *setIdleMessages = NULL;
nn_userhash_t *nnUsers = NULL;
char    *setConfigFile = NULL,
        *setBrowser = NULL;
cfgitem_t *cfg = NULL;


/* Logging mode flags
 */
enum {
    LOG_FILE   = 1,
    LOG_WINDOW = 2,
    LOG_STAMP  = 4
};


/* Arguments
 */
optarg_t optList[] = {
    { 0, '?', "help",       "Show this help", OPT_NONE },
    { 1, 'v', "verbose",    "Be more verbose", OPT_NONE },
    { 2, 'p', "port",       "Connect to port", OPT_ARGREQ },
    { 3, 's', "server",     "Server to connect to", OPT_ARGREQ },
    { 4, 'C', "color",      "Initial color in RGB hex 000000", OPT_ARGREQ },
    { 5, 'l', "logfile",    "Log filename", OPT_ARGREQ },
    { 6, 'D', "daemon",     "A pseudo-daemon mode for logging", OPT_NONE },
    { 7, 'S', "site",       "Site (default: NN)", OPT_ARGREQ },
    { 8, 'd', "debug",      "Enable various debug features", OPT_NONE },
};

const int optListN = (sizeof(optList) / sizeof(optList[0]));


void argShowHelp(void)
{
    th_args_help(stdout, optList, optListN, th_prog_name,
        "[options] <username> <password>");
}


BOOL argHandleOpt(const int optN, char *optArg, char *currArg)
{
    switch (optN) {
    case 0:
        argShowHelp();
        exit(0);
        break;

    case 1:
        th_verbosityLevel++;
        break;
    
    case 2:
        optPort = atoi(optArg);
        break;

    case 3:
        optServer = optArg;
        break;
    
    case 4:
        if ((optUserColor = th_get_hex_triplet(optArg)) < 0) {
            THERR("Invalid color argument '%s', should be a RGB hex triplet '000000'.\n",
                optArg);
            return FALSE;
        }
        THMSG(1, "Using color #%06x\n", optUserColor);
        break;

    case 5:
        optLogFilename = optArg;
        optLogEnable = TRUE;
        break;

    case 7:
        optSite = optArg;
        break;

    case 6:
        optDaemon = TRUE;
        THMSG(1, "Running in pseudo-daemon mode.\n");
        break;

    case 8:
        optDebug = TRUE;
        THMSG(1, "Debug mode enabled.\n");
        break;

    default:
        THERR("Unknown option '%s'.\n", currArg);
        return FALSE;
    }
    
    return TRUE;
}


BOOL argHandleFile(char *currArg)
{
    if (!optUserNameCmd)
        optUserNameCmd = currArg;
    else if (!optPasswordCmd)
        optPasswordCmd = currArg;
    else {
        THERR("Username '%s' already specified on commandline!\n", optUserNameCmd);
        return FALSE;
    }
    
    return TRUE;
}


BOOL getTimeStamp(char *str, size_t len, const char *fmt)
{
    time_t stamp = time(NULL);
    struct tm *stamp_tm;
    if ((stamp_tm = localtime(&stamp)) != NULL) {
        strftime(str, len, fmt, stamp_tm);
        return TRUE;
    } else {
        str[0] = 0;
        return FALSE;
    }
}


nn_window_t *nn_window_new(const char *id)
{
    nn_window_t *res = th_calloc(1, sizeof(nn_window_t));
    
    if (res == NULL) return NULL;

    res->data = th_ringbuf_new(SET_BACKBUF_LEN, th_free);
    if (res->data == NULL) {
        th_free(res);
        return NULL;
    }
    res->id = th_strdup(id);
    res->num = 0;
    
    return res;
}


void nn_window_free(nn_window_t *win)
{
    if (win != NULL) {
        th_ringbuf_free(win->data);
        th_free(win->id);
        th_free(win);
    }
}


nn_window_t *nn_find_window(const char *id)
{
    int i;

    for (i = 0; i < SET_MAX_WINDOWS; i++)
        if (chatWindows[i] != NULL &&
            chatWindows[i]->id != NULL &&
            strcasecmp(id, chatWindows[i]->id) == 0)
            return chatWindows[i];

    return NULL;
}


BOOL openWindow(const char *name, BOOL curwin)
{
    int i;
    nn_window_t *res;
    if (name == NULL)
        return FALSE;

    if ((res = nn_window_new(name)) == NULL)
        return FALSE;

    for (i = 1; i < SET_MAX_WINDOWS; i++)
    if (chatWindows[i] == NULL) {
        res->num = i;
        chatWindows[i] = res;
        if (curwin)
            currWin = res;
        return TRUE;
    }
    
    return FALSE;
}


void closeWindow(nn_window_t *win)
{
    int i;
    if (win == NULL) return;

    for (i = 1; i < SET_MAX_WINDOWS; i++)
    if (chatWindows[i] == win) {
        chatWindows[i] = NULL;
        nn_window_free(win);
        return;
    }
}


void updateStatus(void)
{
    char tmpStr[128];
    int i;
    
    if (statusWin == NULL) return;
    
    getTimeStamp(tmpStr, sizeof(tmpStr), "%H:%M:%S");

    wbkgdset(statusWin, COLOR_PAIR(10));
    werase(statusWin);
    
    wattrset(statusWin, A_BOLD | COLOR_PAIR(11));
    mvwaddstr(statusWin, 0, 1, tmpStr);
    
    wattrset(statusWin, A_BOLD | COLOR_PAIR(13));
    waddstr(statusWin, " | ");
    wattrset(statusWin, A_BOLD | COLOR_PAIR(16));
    waddstr(statusWin, optUserName);
    wattrset(statusWin, A_BOLD | COLOR_PAIR(13));

    wattrset(statusWin, A_BOLD | COLOR_PAIR(13));
    waddstr(statusWin, " | ");
    wattrset(statusWin, A_BOLD | COLOR_PAIR(11));
    snprintf(tmpStr, sizeof(tmpStr), "#%06x", optUserColor);
    waddstr(statusWin, tmpStr);

    wattrset(statusWin, A_BOLD | COLOR_PAIR(13));
    waddstr(statusWin, " | WIN: ");
    snprintf(tmpStr, sizeof(tmpStr), "%d: %s", currWin->num + 1, currWin->id ? currWin->id : "MAIN");
    waddstr(statusWin, tmpStr);

    wattrset(statusWin, A_BOLD | COLOR_PAIR(13));
    waddstr(statusWin, " | ");
    wattrset(statusWin, A_BOLD | COLOR_PAIR(11));

    for (i = 0; i < SET_MAX_WINDOWS; i++)
    if (chatWindows[i] != NULL && chatWindows[i]->dirty) {
        snprintf(tmpStr, sizeof(tmpStr), "%d ", i + 1);
        waddstr(statusWin, tmpStr);
    }

    wrefresh(statusWin);
}


void printEditBuf(nn_editbuf_t *buf)
{
    char *tmp;
    if (editWin == NULL || buf == NULL) return;

    buf->data[buf->len] = 0;
    tmp = nn_username_decode(th_strdup(buf->data));
    
    werase(editWin);
    
    wattrset(editWin, A_NORMAL);
    
    if (buf->pos < buf->len) {
        waddnstr(editWin, tmp, buf->pos);
        wattrset(editWin, A_REVERSE);
        waddch(editWin, tmp[buf->pos]);
        wattrset(editWin, A_NORMAL);
        waddnstr(editWin, tmp + buf->pos + 1, buf->len - buf->pos - 1);
    } else {
        waddnstr(editWin, tmp, buf->len);
        wattrset(editWin, A_REVERSE);
        waddch(editWin, ' ');
        wattrset(editWin, A_NORMAL);
    }
    wrefresh(editWin);
    th_free(tmp);
}


int printWin(WINDOW *win, const char *fmt)
{
    const char *s = fmt;
    int col = 0;

    while (*s) {
        if (*s == '½') {
            int val = 0;
            s++;
            if (*s == '½') {
                waddch(win, ((unsigned char) *s) | col);
                s++;
            } else {
                while (*s >= '0' && *s <= '9') {
                    val *= 10;
                    val += (*s - '0');
                    s++;
                }
                if (*s != '½') return -1;
                s++;

                if (val < 9) {
                    col = A_DIM | COLOR_PAIR(val);
                } else if (val < 30) {
                    col = A_BOLD | COLOR_PAIR(val - 9);
                }
            }
        } else {
            waddch(win, ((unsigned char) *s) | col);
            s++;
        }
    }
    return 0;
}


void nn_window_print(nn_window_t *win, const char *fmt)
{
    const char *s = fmt;
    while (*s) {
        if (*s == '\n') {
            th_vputch(&(win->buf), &(win->bufsize), &(win->len), '\n');
            th_vputch(&(win->buf), &(win->bufsize), &(win->len), 0);
            th_ringbuf_add(win->data, win->buf);
            win->buf = NULL;
            win->dirty = TRUE;
        }
        else    
        if ((unsigned char) *s == 255)
            th_vputch(&(win->buf), &(win->bufsize), &(win->len), ' ');
        else
        if (*s != '\r')
            th_vputch(&(win->buf), &(win->bufsize), &(win->len), *s);
        s++;
    }
}


void updateMainWin(BOOL force)
{
    int y, w, h, offs;
    qringbuf_t *buf;

    if (mainWin == NULL || currWin == NULL) return;
    if (!force && !currWin->dirty) return;

    buf = currWin->data;
    getmaxyx(mainWin, h, w);
    werase(mainWin);
    
    offs = buf->size - h - currWin->pos;
    if (offs < 0)
        offs = 0;

    for (y = 0; y < h && offs < buf->size; offs++) {
        if (buf->data[offs] != NULL)
            printWin(mainWin, (char *) buf->data[offs]);
        y = getcury(mainWin);
    }
    
    currWin->dirty = FALSE;
    wrefresh(mainWin);
}


int printFile(FILE *outFile, const char *fmt)
{
    const char *s = fmt;
    
    while (*s) {
        if (*s == '½') {
            s++;
            if (*s == '½') {
                fputc((unsigned char) *s, outFile);
                s++;
            } else {
                while (*s && isdigit((int) *s)) s++;
                if (*s != '½') return -1;
                s++;
            }
        } else {
            if ((unsigned char) *s == 255)
                fputc(' ', outFile);
            else
                fputc((unsigned char) *s, outFile);
            s++;
        }
    }
    
    return 0;
}

void printMsgV(nn_window_t *win, int flags, const char *fmt, va_list ap)
{
    char tmpStr[128], *buf;
    
    getTimeStamp(tmpStr, sizeof(tmpStr), "½17½[½11½%H:%M:%S½17½]½0½ ");
    
    buf = th_strdup_vprintf(fmt, ap);
    
    if (optLogFile && (flags & LOG_FILE)) {
        if (flags & LOG_STAMP) printFile(optLogFile, tmpStr);
        printFile(optLogFile, buf);
        fflush(optLogFile);
    }
    
    if (!optDaemon && (flags & LOG_WINDOW)) {
        nn_window_t *tmp = win != NULL ? win : chatWindows[0];
        if (flags & LOG_STAMP) nn_window_print(tmp, tmpStr);
        nn_window_print(tmp, buf);
        updateMainWin(FALSE);
    }
    
    th_free(buf);
}

void printMsg(nn_window_t *win, const char *fmt, ...)
{
    va_list ap;
    
    va_start(ap, fmt);
    printMsgV(win, LOG_STAMP | LOG_WINDOW | LOG_FILE, fmt, ap);
    va_end(ap);
}

void printMsgC(nn_window_t *win, const char *fmt, ...)
{
    va_list ap;
    
    va_start(ap, fmt);
    printMsgV(win, LOG_WINDOW | LOG_FILE, fmt, ap);
    va_end(ap);
}

void printMsgQ(nn_window_t *win, BOOL logOnly, const char *fmt, ...)
{
    va_list ap;
    
    va_start(ap, fmt);
    printMsgV(win, logOnly ? (LOG_STAMP | LOG_FILE) : (LOG_STAMP | LOG_WINDOW | LOG_FILE), fmt, ap);
    va_end(ap);
}


char *errorMessages = NULL;

void errorMsgV(const char *fmt, va_list ap)
{
    char *tmp;
    va_list ap2;

    va_copy(ap2, ap);
    printMsgV(chatWindows[0], LOG_STAMP | LOG_WINDOW | LOG_FILE, fmt, ap);

    tmp = th_strdup_vprintf(fmt, ap2);
    
    if (errorMessages != NULL) {
        char *tmp2 = th_strdup_printf("%s%s", errorMessages, tmp);
        th_free(errorMessages);
        th_free(tmp);
        errorMessages = tmp2;
    } else
        errorMessages = tmp;
}

void errorMsg(const char *fmt, ...)
{
    va_list ap;

    va_start(ap, fmt);
    errorMsgV(fmt, ap);
    va_end(ap);
}

void errorFunc(struct _nn_conn_t *conn, const char *fmt, va_list ap)
{
    (void) conn;
    errorMsgV(fmt, ap);
}

void messageFunc(struct _nn_conn_t *conn, const char *fmt, va_list ap)
{
    (void) conn;
    printMsgV(chatWindows[0], LOG_STAMP | LOG_WINDOW | LOG_FILE, fmt, ap);
}


BOOL checkIgnoreList(const char *name)
{
    qlist_t *node = setIgnoreList;
    while (node != NULL) {
        if (strcasecmp(name, (char *) node->data) == 0)
            return TRUE;
        node = node->next;
    }
    return FALSE;
}


int handleUser(nn_conn_t *conn, const char *str)
{
    const char *msg = "</USER><MESSAGE>", *p = str;
    BOOL isMine, isIgnored = FALSE;
    char *s, *t, *userName;
    
    (void) conn;
    
    /* Find start of the message */
    s = strstr(str, msg);
    if (!s) return 1;
    *s = 0;
    s += strlen(msg);
    
    /* Find end of the message */
    t = strstr(s, "</MESSAGE>");
    if (!t) return 3;
    *t = 0;
    
    /* Decode message string */
    s = nn_decode_str1(s);
    if (!s) return -1;
    
    /* Decode username */
    userName = nn_decode_str1(p);
    if (!userName) {
        th_free(s);
        return -2;
    }

    /* Check if the username is on our ignore list and
     * that it is not our OWN username!
     */
    isMine = strcmp(userName, optUserName) == 0;
    isIgnored = setIgnoreMode && !isMine && checkIgnoreList(userName);

    /* Is it a special control message? */
    if (*s == '/') {
        /* Ignore room join/leave messages */
        if (!optDebug && (strstr(s, "left the room") || strstr(s, "joined the room from")))
            goto done;

        t = nn_strip_tags(s + 1);
        if (!strncmp(t, "BPRV ", 5)) {
            char *name, *tmp, *msg, *h;
            nn_window_t *win;
            h = nn_decode_str2(t + 1);
            
            if (!strncmp(t, "BPRV from ", 10)) {
                name = nn_decode_str2(t + 10);
                isMine = FALSE;
            } else {
                name = nn_decode_str2(t + 8);
                isMine = TRUE;
            }

            for (tmp = name; *tmp && *tmp != ':'; tmp++);
            if (tmp[0] != 0 && tmp[1] == ' ')
                msg = tmp + 2;
            else
                msg = "";
            *tmp = 0;

            isIgnored = setIgnoreMode && checkIgnoreList(name);
            win = nn_find_window(name);
            
            if (win != NULL)
                printMsgQ(win, isIgnored, "½5½<½%d½%s½5½>½0½ %s\n", isMine ? 14 : 15, isMine ? optUserName : name, msg);
            else
                printMsgQ(NULL, isIgnored, "½11½%s½0½\n", h);
            
            th_free(h);
        } else {
            /* It's an action (/me) */
            char *h = nn_decode_str2(t);
            printMsgQ(NULL, isIgnored, "½9½* %s½0½\n", h);
            th_free(h);
        }
        th_free(t);
    } else {
        /* It's a normal message */
        char *h;
        t = nn_strip_tags(s);
        h = nn_decode_str2(t);
        printMsgQ(NULL, isIgnored, "½5½<½%d½%s½5½>½0½ %s\n", isMine ? 14 : 15, userName, h);
        th_free(h);
        th_free(t);
    }

done:
    th_free(s);
    th_free(userName);
    return 0;
}


int handleLogin(nn_conn_t *conn, const char *str)
{
    char tmpStr[256];
    
    getTimeStamp(tmpStr, sizeof(tmpStr), "%c");
    
    if (!strncmp(str, "FAILURE", 7)) {
        printMsg(NULL, "½1½Login failure½0½ - ½3½%s½0½\n", tmpStr);
        return -2;
    } else if (!strncmp(str, "SUCCESS", 7)) {
        printMsg(NULL, "½2½Login success½0½ - ½3½%s½0½\n", tmpStr);
        nn_conn_send_msg(conn, optUserNameEnc, "%%2FRequestUserList");
        return 0;
    } else
        return 1;
}


int handleAddUser(nn_conn_t *conn, const char *str)
{
    char *p, *s = strstr(str, "</ADD_USER>");
    nn_window_t *win;

    (void) conn;

    if (!s) return 1;
    *s = 0;
    
    p = nn_dbldecode_str(str);
    if (!p) return -1;

    win = nn_find_window(p);
    nn_userhash_insert(nnUsers, nn_username_encode(p));

    printMsg(NULL, "! ½3½%s½0½ ½2½ADDED.½0½\n", p);
    if (win != NULL)
        printMsg(win, "! ½3½%s½0½ ½2½joined the chat.½0½\n", p);

    th_free(p);
    return 0;
}


int handleDeleteUser(nn_conn_t *conn, const char *str)
{
    char *p, *s = strstr(str, "</DELETE_USER>");
    nn_window_t *win;

    (void) conn;

    if (!s) return 1;
    *s = 0;
    
    p = nn_dbldecode_str(str);
    if (!p) return -1;

    win = nn_find_window(p);
    nn_userhash_delete(nnUsers, nn_username_encode(p));

    printMsg(NULL, "! ½3½%s½0½ ½1½DELETED.½0½\n", p);
    if (win != NULL)
        printMsg(win, "! ½3½%s½0½ ½1½left the chat.½0½\n", p);
        
    th_free(p);
    return 0;
}


int handleFoo(nn_conn_t *conn, const char *str)
{
    (void) conn; (void) str;
    
    return 0;
}


int handleBoot(nn_conn_t *conn, const char *str)
{
    (void) conn; (void) str;
    errorMsg("Booted by server.\n");
    return -1;
}


typedef struct {
    char *cmd;
    ssize_t len;
    int (*handler)(nn_conn_t *, const char *);
} protocmd_t;


static protocmd_t protoCmds[] = {
    { "<USER>",         -1, handleUser },
    { "<LOGIN_",        -1, handleLogin },
    { "<DELETE_USER>",  -1, handleDeleteUser },
    { "<ADD_USER>",     -1, handleAddUser },
    { "<NUMCLIENTS>",   -1, handleFoo },
    { "<BOOT />",       -1, handleBoot },
};

static const int nprotoCmds = sizeof(protoCmds) / sizeof(protoCmds[0]);


int handleProtocol(nn_conn_t *conn, const char *buf, const ssize_t bufLen)
{
    static BOOL protoCmdsInit = FALSE;
    int i;

    if (!protoCmdsInit) {
        for (i = 0; i < nprotoCmds; i++)
            protoCmds[i].len = strlen(protoCmds[i].cmd);
        protoCmdsInit = TRUE;
    }
    
    for (i = 0; i < nprotoCmds; i++) {
        ssize_t cmdLen = protoCmds[i].len;
        if (cmdLen < bufLen && !strncmp(buf, protoCmds[i].cmd, cmdLen))
            return protoCmds[i].handler(conn, buf + cmdLen);
    }

    if (optDebug) {
        printMsg(NULL, "Unknown protocmd: \"%s\"\n", buf);
        return 0;
    } else
        return 1;
}

char * trimLeft(char *buf)
{
    while (*buf != 0 && th_isspace(*buf)) buf++;
    return buf;
}

int compareUsername(const void *s1, const void *s2)
{
    return strcasecmp((char *) s1, (char *) s2);
}

int handleUserInput(nn_conn_t *conn, char *buf, size_t bufLen)
{
    char *tmpStr, tmpBuf[4096];
    BOOL result;
    
    /* Trim right */
    bufLen--;
    buf[bufLen--] = 0;
    while (bufLen > 0 && (buf[bufLen] == '\n' || buf[bufLen] == '\r' || th_isspace(buf[bufLen])))
        buf[bufLen--] = 0;

    /* Decode completed usernames */
    nn_username_decode(buf);
    
    /* Check for special user commands */
    if (*buf == 0) {
        return 1;
    }
    else if (!strncasecmp(buf, "/color ", 7)) {
        /* Change color */
        int tmpInt;
        if ((tmpInt = th_get_hex_triplet(trimLeft(buf + 7))) < 0) {
            printMsg(currWin, "Invalid color value '%s'\n", buf+7);
            return 1;
        }
        optUserColor = tmpInt;
        printMsg(currWin, "Setting color to #%06x\n", optUserColor);
        nn_conn_send_msg(conn, optUserNameEnc, "%%2FSetFontColor%%20%%2Dcolor%%20%06X", optUserColor);
        return 0;
    }
    else if (!strncasecmp(buf, "/ignore", 7)) {
        char *name = trimLeft(buf + 7);
        if (strlen(name) > 0) {
            /* Add or remove someone to/from ignore */
            qlist_t *user = th_llist_find_func(setIgnoreList, name, compareUsername);
            if (user != NULL) {
                printMsg(currWin, "Removed user '%s' from ignore.\n", name);
                th_llist_delete_node(&setIgnoreList, user);
            } else {
                printMsg(currWin, "Now ignoring '%s'.\n", name);
                th_llist_append(&setIgnoreList, th_strdup(name));
            }
        } else {
            /* Just list whomever is in ignore now */
            qlist_t *user = setIgnoreList;
            ssize_t nuser = th_llist_length(setIgnoreList);
            char *result = th_strdup_printf("Users ignored (%d): ", nuser);
            while (user != NULL) {
                if (user->data != NULL) {
                    th_pstr_printf(&result, "%s'%s'", result, (char *) user->data);
                    if (--nuser > 0)
                        th_pstr_printf(&result, "%s, ", result);
                }
                user = user->next;
            }
            printMsg(currWin, "%s\n", result);
            th_free(result);
        }
        return 0;
    }
    else if (!strncasecmp(buf, "/query", 6)) {
        char *name = trimLeft(buf + 6);
        if (strlen(name) > 0) {
            nn_user_t *user = nn_user_find(nnUsers, name);
            if (user != NULL) {
                printMsg(currWin, "Opening PRV query for '%s'.\n", user->name);
                if (openWindow(user->name, TRUE))
                    printMsg(currWin, "In PRV query with '%s'.\n", user->name);
            }
        } else {
            printMsg(currWin, "Usage: /query username\n");
            printMsg(currWin, "To close a PRV query, use /close [username]\n");
            printMsg(currWin, "/close without username will close the current PRV window (if any).\n");
        }
        return 0;
    }
    else if (!strncasecmp(buf, "/close", 6)) {
        char *name = trimLeft(buf + 6);
        if (strlen(name) > 0) {
            nn_window_t *win = nn_find_window(name);
            if (win != NULL) {
                closeWindow(win);
            } else {
                printMsg(currWin, "No PRV query by name '%s'.\n", name);
            }
        } else {
            if (currWin != chatWindows[0]) {
                closeWindow(currWin);
                currWin = chatWindows[0];
            }
        }
        return 0;
    }
    else if (!strncasecmp(buf, "/save", 5)) {
        /* Save configuration */
        FILE *cfgfile = fopen(setConfigFile, "w");
        if (cfgfile == NULL) {
            printMsg(currWin, "Could not create configuration to file '%s': %s\n",
                setConfigFile, strerror(errno));
            return 0;
        }
        printMsg(currWin, "Configuration saved in file '%s', res=%d\n",
            setConfigFile,
            th_cfg_write(cfgfile, setConfigFile, cfg));

        fclose(cfgfile);
        return 0;
    }
    else if (!strncasecmp(buf, "/w ", 3)) {
        /* Open given username's profile via firefox in a new tab */
        char *name = trimLeft(buf + 3);

        printMsg(currWin, "Opening profile for: '%s'\n", name);

        tmpStr = nn_encode_str1(name);
#ifdef __WIN32
        {
        HINSTANCE status;
        snprintf(tmpBuf, sizeof(tmpBuf), "http://www.newbienudes.com/profile/%s/", tmpStr);
        th_free(tmpStr);
        status = ShellExecute(NULL, "open", tmpBuf, NULL, NULL, SW_SHOWNA);
        if (status <= 32)
            printMsg(currWin, "Could not launch default web browser: %d\n", status);
        }
#else
        {
        int status;
        int fds[2];
        pid_t pid;
        snprintf(tmpBuf, sizeof(tmpBuf), "openurl(http://www.newbienudes.com/profile/%s/,new-tab)", tmpStr);
        th_free(tmpStr);

        if (pipe(fds) == -1) {
            int ret = errno;
            printMsg(currWin, "Could not open process communication pipe! (%d, %s)\n", ret, strerror(ret));
            return 0;
        }

        if ((pid = fork()) < 0) {
            printMsg(currWin, "Could not create sub-process!\n");
        } else if (pid == 0) {
            dup2(fds[1], STDOUT_FILENO);
            dup2(fds[0], STDERR_FILENO);
            execlp(setBrowser, setBrowser, "-remote", tmpBuf, (void *)NULL);
            _exit(errno);
        }
        
        wait(&status);
        }
#endif
        return 0;
    }
    else if (!strncasecmp(buf, "/who", 4)) {
        /* Alias /who to /listallusers */
        snprintf(tmpBuf, sizeof(tmpBuf), "/listallusers");
        buf = tmpBuf;
    }
    else if (currWin != chatWindows[0]) {
        if (currWin->id != NULL) {
            snprintf(tmpBuf, sizeof(tmpBuf), "/prv -to %s -msg %s", currWin->id, buf);
            buf = tmpBuf;
        } else {
            printMsg(NULL, "No target set, exiting prv mode.\n");
            return 1;
        }
    }

    /* Send double-encoded */
    tmpStr = nn_dblencode_str(nn_username_decode(buf));
    if (tmpStr == 0) return -2;
    result = nn_conn_send_msg(conn, optUserNameEnc, "%s", tmpStr);
    th_free(tmpStr);
    
    return result ? 0 : -1;
}

void closeWindows(void)
{
    if (mainWin) delwin(mainWin);
    if (statusWin) delwin(statusWin);
    if (editWin) delwin(editWin);
}

BOOL initializeWindows(void)
{
    int w, h;

    getmaxyx(stdscr, h, w);
    
    closeWindows();

    mainWin = subwin(stdscr, h - 4, w, 0, 0);
    statusWin = subwin(stdscr, 1, w, h - 4, 0);
    editWin = subwin(stdscr, 3, w, h - 3, 0);
        
    if (mainWin == NULL || statusWin == NULL || editWin == NULL) {
        THERR("Could not create curses chatWindows!\n");
        return FALSE;
    }
    scrollok(mainWin, 1);
        
    return TRUE;
}

void updateWindows(void)
{
    if (mainWin) redrawwin(mainWin);
    if (statusWin) redrawwin(statusWin);
    if (editWin) redrawwin(editWin);
}

BOOL performTabCompletion(nn_editbuf_t *buf)
{
    static char *previous = NULL, *pattern = NULL;
    BOOL again = FALSE, hasSeparator = FALSE, newPattern = FALSE, hasSpace = FALSE;
    char *str = buf->data;
    int mode = 0;
    ssize_t endPos, startPos = buf->pos;

    /* previous word */
    if (startPos >= 2 && str[startPos - 1] == ' ' && str[startPos - 2] != ' ') {
        startPos -= 2;
        endPos = startPos;
        while (startPos > 0 && str[startPos - 1] != ' ') startPos--;
        mode = 1;
    } else
    /* middle of a word, new pattern */
    if (startPos < buf->len && str[startPos] != ' ') {
        endPos = startPos;
        while (startPos > 0 && str[startPos - 1] != ' ') startPos--;
        while (endPos < buf->len - 1 && str[endPos + 1] != ' ') endPos++;
        newPattern = TRUE;
        mode = 2;
    } else
    /* previous word, new pattern */
    if (startPos >= 1 && str[startPos - 1] != ' ') {
        startPos -= 1;
        endPos = startPos;
        while (startPos > 0 && str[startPos - 1] != ' ') startPos--;
        newPattern = TRUE;
        mode = 3;
    } else {
        if (optDebug)
            printMsg(currWin, "no mode\n");
        return FALSE;
    }

    if (str[endPos] == optNickSep) {
        endPos--;
        if (startPos > 0) {
            if (optDebug)
                printMsg(currWin, "str[endPos] == optNickSep && startPos > 0 (%d)\n", startPos);
            return FALSE;
        }
        hasSeparator = TRUE;
    }

    if (buf->pos > 0 && str[buf->pos - 1] == ' ')
        hasSpace = TRUE;
    if (buf->pos <= buf->len && str[buf->pos] == ' ')
        hasSpace = TRUE;
    
    if (newPattern) {
        /* Get pattern, check if it matches previous pattern and set 'again' flag */
        char *npattern = nn_editbuf_get_string(buf, startPos, endPos);
        if (pattern && npattern && strcasecmp(npattern, pattern) == 0)
            again = TRUE;
        
        th_free(pattern);
        pattern = npattern;
        
        if (!again) {
            th_free(previous);
            previous = NULL;
        }
    }

    if (optDebug) {
        printMsg(currWin, "sPos=%d, ePos=%d <-> bPos=%d, bufLen=%d : pat='%s' (again=%s, hassep=%s, hasspc=%s, newpat=%s, mode=%d)\n",
                  startPos, endPos, buf->pos, buf->len, pattern,
                  again ? "yes" : "no",
                  hasSeparator ? "yes" : "no",
                  hasSpace ? "yes" : "no",
                  newPattern ? "yes" : "no", mode);
    }

    if (pattern) {
        nn_user_t *user = nn_user_match(nnUsers, pattern, previous, again);

        if (user) {
            int i;
            char *c = user->name;
            if (optDebug)
                printMsg(currWin, "match='%s' / prev='%s'\n", user->name, previous);

            for (i = startPos; i <= endPos; i++)
                nn_editbuf_delete(buf, startPos);
                
            for (i = startPos; *c; i++, c++)
                nn_editbuf_insert(buf, i, *c);

            if (!hasSeparator && startPos == 0) {
                nn_editbuf_insert(buf, i++, optNickSep);
                startPos++;
            }
            if (hasSeparator)
                startPos++;
            if (!hasSpace)
                nn_editbuf_insert(buf, i++, ' ');

            nn_editbuf_setpos(buf, startPos + 1 + strlen(user->name));

            th_free(previous);
            previous = th_strdup(user->name);
            
            return TRUE;
        }
    }
    
    return FALSE;
}

#define VPUTCH(CH)  th_vputch(&bufData, &bufSize, &bufLen, CH)
#define VPUTS(STR)  th_vputs(&bufData, &bufSize, &bufLen, STR)

char *logParseFilename(const char *fmt, int id)
{
    size_t bufSize = strlen(fmt) + TH_BUFGROW, bufLen = 0;
    char *bufData = th_malloc(bufSize);
    char tmpBuf[32];
    const char *s = fmt;
    
    while (*s) {
        if (*s == '%') {
            s++;
            switch (*s) {
                case 'i':
                    snprintf(tmpBuf, sizeof(tmpBuf), "%05d", id);
                    VPUTS(tmpBuf);
                    break;

                case 'd':
                    snprintf(tmpBuf, sizeof(tmpBuf), "%d", id);
                    VPUTS(tmpBuf);
                    break;

                case '%':
                    VPUTCH('%');
                    break;
            }
            s++;
        } else {
            VPUTCH(*s);
            s++;
        }
    }
    
    VPUTCH(0);
    return bufData;
}


BOOL logFileOpen(void)
{
    char *filename;
    
    if (optLogFilename == NULL || !optLogEnable)
        return FALSE;

    filename = logParseFilename(optLogFilename, optPort);

    if ((optLogFile = fopen(filename, "a")) == NULL) {
        errorMsg("Could not open logfile '%s' for appending!\n", filename);
        th_free(filename);
        return FALSE;
    }
    
    th_free(filename);

    return TRUE;
}

void logFileClose(void)
{
    if (optLogFile) {
        fclose(optLogFile);
        optLogFile = NULL;
    }
}

char *promptRequester(WINDOW *win, const char *info, BOOL allowEmpty)
{
    char tmpBuf[512], *ptr;
    ssize_t pos;
    int curVis = curs_set(1);

    echo();
    waddstr(win, info);
    wgetnstr(win, tmpBuf, sizeof(tmpBuf) - 1);
    noecho();
    if (curVis != ERR)
        curs_set(curVis);
    
    for (pos = strlen(tmpBuf) - 1; pos > 0 && (tmpBuf[pos] == '\n' || tmpBuf[pos] == '\r' || th_isspace(tmpBuf[pos])); pos--)
        tmpBuf[pos] = 0;

    ptr = trimLeft(tmpBuf);

    if (allowEmpty || strlen(ptr) > 0)
        return th_strdup(ptr);
    else
        return NULL;
}

int main(int argc, char *argv[])
{
    nn_conn_t *conn = NULL;
    struct hostent *tmpHost;
    int curVis = ERR, updateCount = 0;
    BOOL argsOK, isError = FALSE,
        exitProg = FALSE,
        colorSet = FALSE,
        cursesInit = FALSE,
        networkInit = FALSE,
        insertMode = TRUE,
        firstUpdate = TRUE;
    time_t prevTime;
    char *tmpStr;
    nn_editbuf_t *editBuf = nn_editbuf_new(NN_TMPBUF_SIZE);
    nn_editbuf_t *histBuf[SET_MAX_HISTORY+2];
    int histPos = 0, histMax = 0;

    cfgitem_t *tmpcfg;
    char *homeDir = NULL;

    
    /* Initialize */
    th_init("NNChat", "Newbie Nudes chat client", NN_VERSION,
        "Written and designed by Anonymous Finnish Guy (C) 2008-2011",
        "This software is freeware, use and distribute as you wish.");
    th_verbosityLevel = 0;

    /* Read configuration file */
    tmpcfg = NULL;
    th_cfg_add_comment(&tmpcfg, "General settings");
    th_cfg_add_string(&tmpcfg, "username", &optUserName, NULL);
    th_cfg_add_string(&tmpcfg, "password", &optPassword, NULL);
    th_cfg_add_comment(&tmpcfg, "Default color as a hex-triplet");
    th_cfg_add_hexvalue(&tmpcfg, "color", &optUserColor, optUserColor);
    
    th_cfg_add_comment(&tmpcfg, "Default setting of ignore mode");
    th_cfg_add_bool(&tmpcfg, "ignore", &setIgnoreMode, setIgnoreMode);
    th_cfg_add_comment(&tmpcfg, "People to be ignored when ignore mode is enabled");
    th_cfg_add_string_list(&tmpcfg, "ignore_list", &setIgnoreList);

    th_cfg_add_comment(&tmpcfg, "Random messages for idle timeout protection. If none are set, plain '.' is used.");
    th_cfg_add_string_list(&tmpcfg, "idle_messages", &setIdleMessages);

    th_cfg_add_section(&cfg, "general", tmpcfg);


    tmpcfg = NULL;
    th_cfg_add_comment(&tmpcfg, "Chat server hostname or IP address");
    th_cfg_add_string(&tmpcfg, "host", &optServer, optServer);
    th_cfg_add_comment(&tmpcfg, "Default port to connect to (8002 = public room, 8003 = passion pit, 8005 = members only)");
    th_cfg_add_int(&tmpcfg, "port", &optPort, optPort);
    th_cfg_add_section(&cfg, "server", tmpcfg);

    tmpcfg = NULL;
    th_cfg_add_comment(&tmpcfg, "Enable logging");
    th_cfg_add_bool(&tmpcfg, "enable", &optLogEnable, optLogEnable);
    th_cfg_add_comment(&tmpcfg, "Log filename format");
    th_cfg_add_string(&tmpcfg, "filename", &optLogFilename, optLogFilename);
    th_cfg_add_section(&cfg, "logging", tmpcfg);

#ifdef __WIN32
    {
    char tmpPath[MAX_PATH];
    if (SHGetFolderPath(NULL, CSIDL_APPDATA | CSIDL_FLAG_CREATE, NULL, 0, tmpPath) == S_OK)
        homeDir = th_strdup(tmpPath);

    CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);
    }
#else
    homeDir = th_strdup(getenv("HOME"));
#endif
    
    if (homeDir != NULL) {
        FILE *cfgfile;
        setConfigFile = th_strdup_printf("%s" SET_DIR_SEPARATOR "%s", homeDir, SET_CONFIG_FILE);

        THMSG(0, "Reading configuration from '%s'.\n", setConfigFile);

        if ((cfgfile = fopen(setConfigFile, "r")) != NULL)
            th_cfg_read(cfgfile, setConfigFile, cfg);
    }

    setBrowser = getenv("BROWSER");
    if (setBrowser == NULL)
        setBrowser = "firefox";
    
    /* Parse command line arguments */
    argsOK = th_args_process(argc, argv, optList, optListN,
        argHandleOpt, argHandleFile, FALSE);

    if (optUserNameCmd != NULL) {
        optUserName = optUserNameCmd;
        optPassword = optPasswordCmd;
    }

    if (!argsOK)
        return -2;

    /* Allocate userhash */
    if ((nnUsers = nn_userhash_new()) == NULL) {
        THERR("Could not allocate userhash. Fatal error.\n");
        return -105;
    }

    /* If no idle messages are set, add default */
    if (setIdleMessages == NULL) {
        th_llist_append(&setIdleMessages, th_strdup("."));
    }

    /* Open logfile */
    logFileOpen();
        
    /* Initialize network */    
    if (!nn_network_init()) {
        THERR("Could not initialize network subsystem.\n");
        goto err_exit;
    } else
        networkInit = TRUE;

    /* Initialize NCurses */
    if (!optDaemon) {
        if (LINES < 0 || LINES > 1000) LINES = 24;
        if (COLS < 0 || COLS > 1000) COLS = 80;
        initscr();
        raw();
        keypad(stdscr, TRUE);
        noecho();
        meta(stdscr, TRUE);
        timeout(SET_DELAY);
        curVis = curs_set(0);

        if (has_colors()) {
            start_color();
            
            init_pair( 1, COLOR_RED,     COLOR_BLACK);
            init_pair( 2, COLOR_GREEN,   COLOR_BLACK);
            init_pair( 3, COLOR_YELLOW,  COLOR_BLACK);
            init_pair( 4, COLOR_BLUE,    COLOR_BLACK);
            init_pair( 5, COLOR_MAGENTA, COLOR_BLACK);
            init_pair( 6, COLOR_CYAN,    COLOR_BLACK);
            init_pair( 7, COLOR_WHITE,   COLOR_BLACK);
            init_pair( 8, COLOR_BLACK,   COLOR_BLACK);

            init_pair(10, COLOR_BLACK,   COLOR_RED);
            init_pair(11, COLOR_WHITE,   COLOR_RED);
            init_pair(12, COLOR_GREEN,   COLOR_RED);
            init_pair(13, COLOR_YELLOW,  COLOR_RED);
            init_pair(14, COLOR_BLUE,    COLOR_RED);
            init_pair(15, COLOR_MAGENTA, COLOR_RED);
            init_pair(16, COLOR_CYAN,    COLOR_RED);
        }
        
        cursesInit = TRUE;
        
        if (!initializeWindows())
            goto err_exit;

        memset(chatWindows, 0, sizeof(chatWindows));
        chatWindows[0] = nn_window_new(NULL);
        currWin = chatWindows[0];        
        updateStatus();
    }

    /* Check if we have username and password */
    if (cursesInit && (optUserName == NULL || optPassword == NULL)) {
        printWin(editWin, "You can avoid this prompt by issuing '/save' after logging in.\n");
        optUserName = promptRequester(editWin, "NN username: ", FALSE);
        optPassword = promptRequester(editWin, "NN password: ", TRUE);
    }
    
    if (optUserName == NULL || optPassword == NULL) {
        errorMsg("Username and/or password not specified.\n");
        goto err_exit;
    }

    /* Okay ... */
    printMsg(currWin, "Trying to resolve host '%s' ...\n", optServer);
    tmpHost = gethostbyname(optServer);
    if (tmpHost == NULL) {
        errorMsg("Could not resolve hostname: %s.\n", strerror(h_errno));
        goto err_exit;
    }
    printMsg(currWin, "True hostname: %s\n", tmpHost->h_name);

    /* To emulate the official client, we first make a request for
     * policy file, even though we don't use it for anything...
     */
    conn = nn_conn_open((struct in_addr *) tmpHost->h_addr, 843);
    if (!nn_conn_check(conn)) {
        errorMsg("Policy file request connection setup failed!\n");
        goto err_exit;
    }

    tmpStr = "<policy-file-request/>";
    if (nn_conn_send_buf(conn, tmpStr, strlen(tmpStr) + 1) == FALSE) {
        errorMsg("Failed to send policy file request.\n");
        goto err_exit;
    } else {
        int cres = nn_conn_pull(conn);
        if (cres == 0) {
            printMsg(currWin, "Probe got: %s\n", conn->buf);
        } else {
            printMsg(currWin, "Could not get policy probe.\n");
        }
    }
    nn_conn_close(conn);

    /* Okay, now do the proper connection ... */
    conn = nn_conn_open((struct in_addr *) tmpHost->h_addr, optPort);
    if (!nn_conn_check(conn)) {
        errorMsg("Main connection setup failed!\n");
        goto err_exit;
    }
    
    conn->errfunc = errorFunc;
    conn->msgfunc = messageFunc;

    /* Send login command */
    optUserNameEnc = nn_dblencode_str(optUserName);
    tmpStr = nn_dblencode_str(optSite);
    nn_conn_send_msg(conn, optUserNameEnc, "%%2Flogin%%20%%2Dsite%%20%s%%20%%2Dpassword%%20%s", tmpStr, optPassword);
    th_free(tmpStr);
    
    /* Initialize random numbers */
    prevTime = time(NULL);
    srandom((int) prevTime);

    if (cursesInit) {
        /* Initialize rest of interactive UI code */
        memset(histBuf, 0, sizeof(histBuf));
        nn_editbuf_clear(editBuf);

        /* First update of screen */
        printEditBuf(editBuf);
        updateStatus();

        printMsg(NULL, "%s v%s - %s\n", th_prog_name, th_prog_version, th_prog_fullname);
        printMsg(NULL, "%s\n", th_prog_author);
        printMsg(NULL, "%s\n", th_prog_license);
    }

    /* Enter mainloop */
    while (!isError && !exitProg) {
        int cres = nn_conn_pull(conn);
        if (cres == 0) {
            do {
                size_t bufLen = strlen(conn->ptr) + 1;
                int result = handleProtocol(conn, conn->ptr, bufLen);

                if (result > 0) {
                    /* Couldn't handle the message for some reason */
                    printMsg(currWin, "Could not handle: %s\n", conn->ptr);
                } else if (result < 0) {
                    /* Fatal error, quit */
                    errorMsg("Fatal error with message: %s\n", conn->ptr);
                    isError = TRUE;
                }

                conn->got -= bufLen;
                conn->ptr += bufLen;
            } while (conn->got > 0 && !isError);
        }
        if (!nn_conn_check(conn))
            isError = TRUE;

        /* Handle user input */
        if (cursesInit) {
            int c, cnt = 0;
            BOOL update = FALSE, updateMain = TRUE;
            
            /* Handle several buffered keypresses at once */
            do {
            c = wgetch(stdscr);
            if (c == 0x1b) {
                c = wgetch(stdscr);
                if (c == 'O') {
                    c = wgetch(stdscr);
                    switch (c) {
                        case 'd': c = 0x204; break;
                        case 'c': c = 0x206; break;
                        default:
                            if (optDebug)
                                printMsg(currWin, "Unhandled ESC-O key sequence 0x%02x\n", c);
                            break;
                    }
                } else
                if (c == '[') {
                    c = wgetch(stdscr);
                    switch (c) {
                        case 0x31:
                            c = wgetch(stdscr);
                            if (c >= 0x31 && c <= 0x39)
                                c = KEY_F(c - 0x30);
                            else
                                c = ERR;
                            break;
                        
                        case 0x32: c = KEY_IC; break;
                        case 0x33: c = KEY_DC; break;

                        case 0x35: c = KEY_PPAGE; break;
                        case 0x36: c = KEY_NPAGE; break;

                        case 0x37: c = KEY_HOME; break;
                        case 0x38: c = KEY_END; break;
                        
                        default:
                            if (optDebug)
                                printMsg(currWin, "Unhandled ESC-[*~ key sequence 0x%02x\n", c);
                            c = ERR;
                            break;
                    }
                    /* Get the trailing ~ */
                    if (c != ERR)
                        wgetch(stdscr);
                }
                if (c >= 0x31 && c <= 0x39) {
                    /* Chat window switching via Meta/Esc-[1..9] */
                    int win = c - 0x31;
                    if (win < SET_MAX_WINDOWS && chatWindows[win] != NULL) {
                        currWin = chatWindows[win];
                        update = updateMain = TRUE;
                    }
                    c = ERR;
                }
                else {
                    if (optDebug)
                        printMsg(currWin, "Unhandled ESC key sequence 0x%02x\n", c);
                    continue;
                }
            }
            

            switch (c) {
#ifdef KEY_RESIZE
            case KEY_RESIZE:
                resize_term(0, 0);
                erase();
#ifdef PDCURSES
                timeout(SET_DELAY);
#endif
                                
                if (!initializeWindows()) {
                    errorMsg("Error resizing curses chatWindows\n");
                    isError = TRUE;
                }

                update = TRUE;
                updateMain = TRUE;
                break;
#endif
            
            case KEY_ENTER:
            case '\n':
            case '\r':
                /* Call the user input handler */
                if (editBuf->len > 0) {
                    int result;
                    
                    if (histMax > 0) {
                        nn_editbuf_free(histBuf[SET_MAX_HISTORY+1]);
                        histBuf[SET_MAX_HISTORY+1] = NULL;
                        memmove(&histBuf[2], &histBuf[1], histMax * sizeof(histBuf[0]));
                    }
                    
                    histPos = 0;
                    histBuf[1] = nn_editbuf_copy(editBuf);
                    if (histMax < SET_MAX_HISTORY) histMax++;
                    
                    nn_editbuf_insert(editBuf, editBuf->len, 0);
                    result = handleUserInput(conn, editBuf->data, editBuf->len);
                    
                    nn_editbuf_clear(editBuf);
                    
                    if (result < 0) {
                        errorMsg("Fatal error handling user input: %s\n", editBuf->data);
                        isError = TRUE;
                    } else {
                        /* Update time value of last sent message for unidle timeouts */
                        prevTime = time(NULL);
                    }
                    
                    update = TRUE;
                }
                break;
            
            case KEY_UP: /* Backwards in input history */
                if (histPos == 0) {
                    nn_editbuf_free(histBuf[0]);
                    histBuf[0] = nn_editbuf_copy(editBuf);
                }
                if (histPos < histMax) {
                    histPos++;
                    nn_editbuf_free(editBuf);
                    editBuf = nn_editbuf_copy(histBuf[histPos]);
                    update = TRUE;
                }
                break;
                
            case KEY_DOWN: /* Forwards in input history */
                if (histPos > 0) {
                    histPos--;
                    nn_editbuf_free(editBuf);
                    editBuf = nn_editbuf_copy(histBuf[histPos]);
                    update = TRUE;
                }
                break;
                
            case 0x204: /* ctrl+left arrow = Skip words left */
            case 0x20b:
                while (editBuf->pos > 0 && isspace((int) editBuf->data[editBuf->pos - 1]))
                    editBuf->pos--;
                while (editBuf->pos > 0 && !isspace((int) editBuf->data[editBuf->pos - 1]))
                    editBuf->pos--;
                update = TRUE;
                break;
            
            case 0x206: /* ctrl+right arrow = Skip words right */
            case 0x210:
                while (editBuf->pos < editBuf->len && isspace((int) editBuf->data[editBuf->pos]))
                    editBuf->pos++;
                while (editBuf->pos < editBuf->len && !isspace((int) editBuf->data[editBuf->pos]))
                    editBuf->pos++;
                update = TRUE;
                break;

            case KEY_HOME:  nn_editbuf_setpos(editBuf, 0); update = TRUE; break;
            case KEY_END:   nn_editbuf_setpos(editBuf, editBuf->len); update = TRUE; break;
            case KEY_LEFT:  nn_editbuf_setpos(editBuf, editBuf->pos - 1); update = TRUE; break;
            case KEY_RIGHT: nn_editbuf_setpos(editBuf, editBuf->pos + 1); update = TRUE; break;
            
            case KEY_BACKSPACE:
            case 0x08:
            case 0x7f:
                nn_editbuf_delete(editBuf, editBuf->pos - 1);
                nn_editbuf_setpos(editBuf, editBuf->pos - 1);
                update = TRUE;
                break;
            
            case KEY_DC: /* Delete character */
                nn_editbuf_delete(editBuf, editBuf->pos);
                update = TRUE;
                break;
                

            case KEY_IC: /* Ins = Toggle insert / overwrite mode */
                insertMode = !insertMode;
                update = TRUE;
                break;
            
            case KEY_F(2): /* F2 = Clear editbuffer */
                nn_editbuf_clear(editBuf);
                update = TRUE;
                break;

            case KEY_F(5): /* F5 = Ignore mode */
                setIgnoreMode = !setIgnoreMode;
                printMsg(currWin, "Ignore mode = %s\n", setIgnoreMode ? "ON" : "OFF");
                break;
            
            case 0x03: /* ^C = quit */
            case KEY_F(9): /* F9 = Quit */
                printMsg(currWin, "Quitting per user request.\n");
                exitProg = TRUE;
                break;
            
            case 0x09: /* Tab = complete username */
                performTabCompletion(editBuf);
                update = TRUE;
                break;
            
            case 0x0c: /* Ctrl + L */
                updateWindows();
                update = TRUE;
                updateMain = TRUE;
                break;

            case KEY_NPAGE:
            case KEY_PPAGE:
                if (currWin != NULL)
                {
                int numLines, numCols, oldPos = currWin->pos;
                getmaxyx(mainWin, numLines, numCols);
                numLines = (numLines / 2) + 1;

                if (c == KEY_PPAGE)
                    currWin->pos = (currWin->pos > numLines) ? currWin->pos - numLines : 0;
                else
                    currWin->pos = (currWin->pos < currWin->data->n - numLines) ? currWin->pos + numLines : currWin->data->n - numLines;

                if (oldPos != currWin->pos)
                    updateMain = TRUE;
                }
                break;

            case ERR:
                /* Ignore */
                break;
                
            default:
                if (isprint(c) || c == 0xe4 || c == 0xf6 || c == 0xc4 || c == 0xd6) {
                    if (insertMode)
                        nn_editbuf_insert(editBuf, editBuf->pos, c);
                    else
                        nn_editbuf_write(editBuf, editBuf->pos, c);
                    nn_editbuf_setpos(editBuf, editBuf->pos + 1);
                    update = TRUE; 
                } else {
                    if (optDebug)
                        printMsg(currWin, "Unhandled key: 0x%02x\n", c);
                }
                break;
            }
            } while (c != ERR && !exitProg && ++cnt < 10);
            
            if (update || firstUpdate) {
                /* Update edit line */
                printEditBuf(editBuf);
                updateStatus();
                firstUpdate = FALSE; /* a nasty hack ... */
            }
            
            updateMainWin(updateMain);
        } /* cursesInit */
        
        if (++updateCount > 10) {
            time_t tmpTime = time(NULL);
            if (tmpTime - prevTime > SET_KEEPALIVE) {
                int n = random() % th_llist_length(setIdleMessages);
                qlist_t *node = th_llist_get_nth(setIdleMessages, n);
                nn_conn_send_msg(conn, optUserNameEnc, node->data);
                prevTime = tmpTime;
            }
            
            if (!colorSet) {
                colorSet = TRUE;
                nn_conn_send_msg(conn, optUserNameEnc, "%%2FSetFontColor%%20%%2Dcolor%%20%06X", optUserColor);
            }
            
            updateStatus();
            updateCount = 0;
        }
        
    }
    
    /* Shutdown */
err_exit:
    nn_userhash_free(nnUsers);
    nn_editbuf_free(editBuf);

    {
    int i;
    for (i = 0; i <= SET_MAX_HISTORY; i++)
        nn_editbuf_free(histBuf[i]);

    for (i = 0; i < SET_MAX_WINDOWS; i++)
        nn_window_free(chatWindows[i]);
    }

#ifdef __WIN32
    if (errorMessages) {
        char *tmp;
        wclear(editWin);
        tmp = promptRequester(editWin, "Press enter to quit.\n", FALSE);
        th_free(tmp);
    }
#endif

    if (cursesInit) {
        if (curVis != ERR)
            curs_set(curVis);
        closeWindows();
        endwin();
        THMSG(1, "NCurses deinitialized.\n");
    }
    
#ifndef __WIN32
    if (errorMessages)
        THERR("%s", errorMessages);
#endif
    
    th_free(optUserNameEnc);

    nn_conn_close(conn);
    
    if (networkInit)
        nn_network_close();

    THMSG(1, "Connection terminated.\n");
    
    logFileClose();

    return 0;
}