view nnchat.c @ 322:b9c15c57dc8f

Clean up message functions, add new printMsgQ() helper function for messages that should not go into the log file. Add skeleton help function, accessible via F1 key. And other cleanups.
author Matti Hamalainen <ccr@tnsp.org>
date Sat, 11 Jun 2011 09:48:26 +0300
parents 384d508d1df3
children 863e3a26974d
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);
    
    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 / %d",
        currWin->num + 1,
        currWin->id != NULL ? currWin->id : "MAIN",
        currWin->pos);
    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);
    }
    
    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 printMsgF(nn_window_t *win, int flags, const char *fmt, ...)
{
    va_list ap;
    
    va_start(ap, fmt);
    printMsgV(win, flags | LOG_STAMP, fmt, ap);
    va_end(ap);
}

void printMsgQ(nn_window_t *win, const char *fmt, ...)
{
    va_list ap;
    
    va_start(ap, fmt);
    printMsgV(win, LOG_STAMP | LOG_WINDOW, 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) {
                printMsgF(win, isIgnored ? LOG_FILE : (LOG_WINDOW | LOG_FILE),
                    "½5½<½%d½%s½5½>½0½ %s\n",
                    isMine ? 14 : 15, isMine ? optUserName : name, msg);
            } else {
                printMsgF(NULL, isIgnored ? LOG_FILE : (LOG_WINDOW | LOG_FILE),
                    "½11½%s½0½\n", h);
            }
            th_free(name);
            th_free(h);
        } else {
            /* It's an action (/me) */
            char *h = nn_decode_str2(t);
            printMsgF(NULL, isIgnored ? LOG_FILE : (LOG_WINDOW | LOG_FILE),
                "½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);
        printMsgF(NULL, isIgnored ? LOG_FILE : (LOG_WINDOW | LOG_FILE),
            "½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) {
            printMsgQ(currWin, "Invalid color value '%s'\n", buf+7);
            return 1;
        }
        optUserColor = tmpInt;
        printMsgQ(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) {
                printMsgQ(currWin, "Removed user '%s' from ignore.\n", name);
                th_llist_delete_node(&setIgnoreList, user);
            } else {
                printMsgQ(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;
            }
            printMsgQ(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, nn_username_encode(name));
            if (user != NULL) {
                name = nn_username_decode(th_strdup(user->name));
                printMsgQ(currWin, "Opening PRV query for '%s'.\n", name);
                if (openWindow(name, TRUE))
                    printMsgQ(currWin, "In PRV query with '%s'.\n", name);
                th_free(name);
            }
        } else {
            printMsgQ(currWin, "Usage: /query username\n");
            printMsgQ(currWin, "To close a PRV query, use /close [username]\n");
            printMsgQ(currWin, "/close without username will close the current PRV window.\n");
        }
        return 0;
    }
    else if (!strncasecmp(buf, "/win", 4)) {
        /* Change color */
        char *tmp = trimLeft(buf + 4);
        if (strlen(tmp) > 0) {
            int val = atoi(tmp);
            if (val >= 1 && val < SET_MAX_WINDOWS) {
                if (chatWindows[val - 1] != NULL)
                    currWin = chatWindows[val - 1];
            } else {
                printMsgQ(currWin, "Invalid window number '%s'\n", tmp);
                return 1;
            }
        } else {
            printMsgQ(currWin, "Window   : #%d\n", currWin->num);
            printMsgQ(currWin, "ID       : %s\n", currWin->id);
        }
        return 0;
    }
    else if (!strncasecmp(buf, "/close", 6)) {
        char *name = trimLeft(buf + 6);
        if (strlen(name) > 0) {
            nn_window_t *win;
            win = nn_find_window(name);
            if (win != NULL) {
                closeWindow(win);
                printMsgQ(currWin, "Closed PRV query to '%s'.\n", name);
            } else {
                printMsgQ(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) {
            printMsgQ(currWin, "Could not create configuration to file '%s': %s\n",
                setConfigFile, strerror(errno));
            return 0;
        }
        printMsgQ(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)
            printMsgQ(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;
            printMsgQ(currWin, "Could not open process communication pipe! (%d, %s)\n", ret, strerror(ret));
            return 0;
        }

        if ((pid = fork()) < 0) {
            printMsgQ(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 {
            printMsgQ(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)
        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;
}

void printHelp(void)
{
    printMsgQ(currWin,
    "NNChat Help\n"
    "===========\n"
    "\n"
    "F1                This help.\n"
    );
}

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;

    memset(histBuf, 0, sizeof(histBuf));
    
    /* 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 (8005 = main room, 8003 = passion pit)");
    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);
            fclose(cfgfile);
        }
    }

    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 */
        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 = FALSE;
            
            /* 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);
                timeout(SET_DELAY);

                if (!initializeWindows()) {
                    errorMsg("Error resizing curses chatWindows\n");
                    isError = TRUE;
                }
                update = 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);
                    }
                    
                    updateMain = 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(1): /* F1 = Print help */
                printHelp();
                updateMain = 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 = 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:
    th_cfg_free(cfg);
    th_free(homeDir);
    th_llist_free_func(setIdleMessages, th_free);
    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;
}