
 /* ==================================================================

    FILE: "/home/joze/src/tclreadline/tclreadline.c"
    LAST MODIFICATION: "Tue Aug 31 03:22:55 1999 (joze)"
    (C) 1998, 1999 by Johannes Zellner, <johannes@zellner.org>
    $Id$
    ---

    tclreadline -- gnu readline for tcl
    Copyright (C) 1999  Johannes Zellner

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

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

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

    <johannes@zellner.org>, http://www.zellner.org/tclreadline/

    ================================================================== */  

#include <tcl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define READLINE_LIBRARY
#include <readline.h>
#include <history.h>

#include "tclreadline.h"

#define MALLOC(size) Tcl_Alloc((int) size)
#define FREE(ptr) if (ptr) { Tcl_Free((char*) ptr); ptr = 0; }

enum {
    _CMD_SET     = (1 << 0),
    _CMD_GET     = (1 << 1)
};


typedef struct cmds_t {
    struct cmds_t* prev;
    char**         cmd;
    struct cmds_t* next;
} cmds_t;


#define ISWHITE(c) ((' ' == c) || ('\t' == c) || ('\n' == c))

/*
 * forward declarations.
 */
char* stripleft(char* in);
char* stripright(char* in);
char* stripwhite(char* in);
char* TclReadlineQuote(char* text, char* quotechars);
int TclReadlineCmd(ClientData clientData, Tcl_Interp* interp,
    int argc, char** argv);
void TclReadlineDataAvailableHandler(ClientData clientData, int mask);
void TclReadlineLineCompleteHandler(char* ptr);
int Tclreadline_SafeInit(Tcl_Interp* interp);
int Tclreadline_Init(Tcl_Interp* interp);
char *TclReadlineFilenameQuotingFunction(
    char *text, int match_type, char* quote_ptr);
int TclReadlineInitialize(Tcl_Interp* interp, char* historyfile);
int blank_line(char* str);
char** TclReadlineCompletion(char* text, int start, int end);
char* TclReadline0generator(char* text, int state);
char* TclReadlineKnownCommands(char* text, int state, int mode);
int TclReadlineParse(char** args, int maxargs, char* buf);

enum { 
    LINE_PENDING,
    LINE_EOF,
    LINE_COMPLETE
};

/**
 * global variables
 */
static int tclrl_line_complete = LINE_PENDING;
static int tclrl_state = TCL_OK;
static char* tclrl_eof_string = (char*) NULL;
static char* tclrl_line = (char*) NULL;
static char* tclrl_custom_completer = (char*) NULL;
static int tclrl_use_builtin_completer = 1;
static int tclrl_history_length = -1;
Tcl_Interp* tclrl_interp = (Tcl_Interp*) NULL;


char*
stripleft(char* in)
{
    char* ptr = in;
    while (*ptr && *ptr <= ' ')
        ptr++;
    if (in != ptr)
        memmove(in, ptr, strlen(ptr) + 1);
    return in;
}

char*
stripright(char* in)
{
    char* ptr;
    for (ptr = strchr(in, '\0') - 1; ptr >= in && *ptr <= ' '; ptr--)
        *ptr = '\0';
    return in;
}

char*
stripwhite(char* in)
{
    stripleft(in);
    stripright(in);
    return in;
}

char*
TclReadlineQuote(char* text, char* quotechars)
{
    char* ptr;
    char* result_c;
    int i, len = strlen(quotechars);
    Tcl_DString result;

    Tcl_DStringInit(&result);
    for (ptr = text; ptr && *ptr; ptr++) {
        for (i = 0; i < len; i++) {
            if (quotechars[i] == *ptr) {
                Tcl_DStringAppend(&result, "\\", 1);
                break;
            }
        }
        Tcl_DStringAppend(&result, ptr, 1);
    }
    result_c = strdup(Tcl_DStringValue(&result));
    return result_c;
}

int
TclReadlineCmd(
    ClientData  clientData, /* Main window associated with interpreter  */
    Tcl_Interp* interp,     /* Current interpreter                      */
    int         argc,       /* Number of arguments                      */
    char**      argv        /* Argument strings                         */
)
{
    int		c, length;
    
    if (argc < 2)
        goto BAD_COMMAND;

    c = argv[1][0];
    length = strlen(argv[1]);

    if (c == 'r'  && strncmp(argv[1], "read", length) == 0) {
        
        char* expansion = (char*) NULL;
        int status;
        
        tclrl_line_complete = LINE_PENDING;
        tclrl_state = TCL_OK;
        rl_callback_handler_install(argc == 3 ? argv[2] : "%",
                TclReadlineLineCompleteHandler);

        Tcl_CreateFileHandler(0, TCL_READABLE,
                TclReadlineDataAvailableHandler, (ClientData) NULL);

        /**
         * Main Loop.
         * XXX each modification of the global variables
         *     which terminates the main loop must call
         *     rl_callback_handler_remove() to leave
         *     readline in a defined state.          XXX
         */
        while (LINE_PENDING == tclrl_line_complete
            && TCL_OK == tclrl_state && !rl_done) {
            Tcl_DoOneEvent(0);
            /*
            rl_inhibit_completion = 0;
            */
        }
        Tcl_DeleteFileHandler(0);

        if (TCL_OK != tclrl_state)
            return tclrl_state; /* !! */

	if ((LINE_EOF == tclrl_line_complete) && tclrl_eof_string) {
	    Tcl_Eval(interp, tclrl_eof_string);
            return tclrl_state;
	}

        status = history_expand(tclrl_line, &expansion);
        if (status >= 1) {
#if 0
            Tcl_Channel channel = Tcl_MakeFileChannel(stdout, TCL_WRITABLE);
            /* Tcl_RegisterChannel(interp, channel); */
            (void) Tcl_WriteChars(channel, expansion, -1);
            Tcl_Flush(channel);
            Tcl_Close(interp, channel);
#else
            printf("%s\n", expansion);
#endif
        }
        else if (status == -1) {
            Tcl_AppendResult
                (interp, "error in history expansion\n", (char*) NULL);
            return tclrl_state;
        }
        /**
         * TODO: status == 2 ...
         */
        
        if (expansion && *expansion)
            add_history(expansion);

        Tcl_SetResult(interp, expansion, TCL_VOLATILE);

        FREE(tclrl_line);
        FREE(expansion);
        return tclrl_state;
    } else if (c == 'i'  && strncmp(argv[1], "initialize", length) == 0) {
        if (3 != argc)
            goto BAD_COMMAND;
        else
            return TclReadlineInitialize(interp, argv[2]);
    } else if (c == 'w'  && strncmp(argv[1], "write", length) == 0) {
        if (3 != argc) {
            goto BAD_COMMAND;
        }  else if (write_history(argv[2])) {
            Tcl_AppendResult(interp, "unable to write history to `",
                    argv[2], "'\n", (char*) NULL);
            return TCL_ERROR;
        }
        if (tclrl_history_length >= 0) {
            history_truncate_file(argv[2], tclrl_history_length);
        }
        return TCL_OK;
    } else if (c == 'a'  && strncmp(argv[1], "add", length) == 0) {
        if (3 != argc)
            goto BAD_COMMAND;
        else if (TclReadlineKnownCommands(argv[2], (int) 0, _CMD_SET))
            Tcl_AppendResult(interp, "unable to add command \"",
                    argv[2], "\"\n", (char*) NULL);
    } else if (c == 'c'  && strncmp(argv[1], "complete", length) == 0) {
        if (3 != argc)
            goto BAD_COMMAND;
        else if (Tcl_CommandComplete(argv[2]))
            Tcl_AppendResult(interp, "1", (char*) NULL);
        else
            Tcl_AppendResult(interp, "0", (char*) NULL);
    } else if (c == 'c'  && strncmp(argv[1], "customcompleter", length) == 0) {
        if (3 != argc && 2 != argc)
            goto BAD_COMMAND;
        if (3 == argc) {
            if (tclrl_custom_completer)
                FREE(tclrl_custom_completer);
            if (!blank_line(argv[2]))
                tclrl_custom_completer = stripwhite(strdup(argv[2]));
        }
        Tcl_AppendResult (interp, tclrl_custom_completer, (char*) NULL);
    } else if (c == 'b'  && strncmp(argv[1], "builtincompleter", length) == 0) {
        int bool = tclrl_use_builtin_completer;
        if (3 != argc && 2 != argc)
            goto BAD_COMMAND;
        if (3 == argc) {
            if (TCL_OK != Tcl_GetBoolean(interp, argv[2], &bool)) {
                Tcl_AppendResult(interp,
                    "wrong # args: should be a boolean value.", (char*) NULL);
                return TCL_ERROR;
            } else {
                tclrl_use_builtin_completer = bool;
            }
        }
        Tcl_AppendResult(interp, tclrl_use_builtin_completer ? "1" : "0",
            (char*) NULL);
    } else if (c == 'e'  && strncmp(argv[1], "eofchar", length) == 0) {
        if (3 != argc && 2 != argc)
            goto BAD_COMMAND;
        if (3 == argc) {
            if (tclrl_eof_string)
                FREE(tclrl_eof_string);
            if (!blank_line(argv[2]))
                tclrl_eof_string = stripwhite(strdup(argv[2]));
        }
        Tcl_AppendResult(interp, tclrl_eof_string, (char*) NULL);
    } else {
        goto BAD_COMMAND;
    }

    return TCL_OK;

BAD_COMMAND:
    Tcl_AppendResult(interp,
        "wrong # args: should be \"readline option ?arg ...?\"",
        (char*) NULL);
    return TCL_ERROR;

}

void
TclReadlineDataAvailableHandler(ClientData clientData, int mask)
{
#if 0
    fprintf(stderr, "(TclReadlineDataAvailableHandler) mask = %d\n",  mask);
#endif
    if (mask & TCL_READABLE) {
        rl_callback_read_char();
    }
}

void
TclReadlineLineCompleteHandler(char* ptr)
{
#if 1
    if (!ptr) { /* <c-d> */
        tclrl_line_complete = LINE_EOF;
        rl_callback_handler_remove();
    } else if (*ptr) {
        tclrl_line_complete = LINE_COMPLETE;
        rl_callback_handler_remove();
        tclrl_line = ptr;
    }
#else
    if (ptr && *ptr) {
        tclrl_line_complete = 1;
        rl_callback_handler_remove();
        tclrl_line = ptr;
    }
#endif
}

int
Tclreadline_SafeInit(Tcl_Interp *interp)
{
    return Tclreadline_Init(interp);
}

int
Tclreadline_Init(Tcl_Interp *interp)
{
    int status;
    Tcl_CreateCommand(interp, "::tclreadline::readline", TclReadlineCmd,
	    (ClientData) NULL, (Tcl_CmdDeleteProc *) NULL);
    tclrl_interp = interp;
    if (TCL_OK != (status = Tcl_LinkVar(interp, "::tclreadline::historyLength",
                (char*) &tclrl_history_length, TCL_LINK_INT)))
        return status;
    if (TCL_OK != (status = Tcl_LinkVar(interp, "::tclreadline::library",
         (char*) &TCLRL_LIBRARY, TCL_LINK_STRING | TCL_LINK_READ_ONLY)))
        return status;
    if (TCL_OK != (status = Tcl_LinkVar(interp, "::tclreadline::version",
         (char*) &TCLRL_VERSION, TCL_LINK_STRING | TCL_LINK_READ_ONLY)))
        return status;
    if (TCL_OK != (status = Tcl_LinkVar(interp, "::tclreadline::patchLevel",
         (char*) &TCLRL_PATCHLEVEL, TCL_LINK_STRING | TCL_LINK_READ_ONLY)))
        return status;
    if (TCL_OK != (status = Tcl_LinkVar(interp, "tclreadline_library",
         (char*) &TCLRL_LIBRARY, TCL_LINK_STRING | TCL_LINK_READ_ONLY)))
        return status;
    if (TCL_OK != (status = Tcl_LinkVar(interp, "tclreadline_version",
         (char*) &TCLRL_VERSION, TCL_LINK_STRING | TCL_LINK_READ_ONLY)))
        return status;
    if (TCL_OK != (status = Tcl_LinkVar(interp, "tclreadline_patchLevel",
         (char*) &TCLRL_PATCHLEVEL, TCL_LINK_STRING | TCL_LINK_READ_ONLY)))
        return status;
    return Tcl_PkgProvide(interp, "tclreadline", TCLRL_VERSION);
}

#if 0
char *
TclReadlineFilenameQuotingFunction
(char *filename, int match_type, char* quote_ptr)
{
    char *res = (char*) malloc(sizeof(char) * (strlen(filename) + 2));
    int i = 0;
    fprintf (stderr, "(TclReadlineFilenameQuotingFunction) \n");
    if (quote_ptr && *quote_ptr) {
        *res = *quote_ptr;                     /* leading quote */
        i++;
    }
    strcpy (res + i, filename);              /* name          */
#if 0
    fprintf (stderr, "(Tclreadline_Init) filename=|%s|\n", filename);
    fprintf (stderr, "(Tclreadline_Init) *quote_ptr=|%c|\n", *quote_ptr);
#endif
    if (quote_ptr && '{' == *quote_ptr) {
        *quote_ptr = '}';
    }
    return res;

#if 0
    switch (match_type) {
        case SINGLE_MATCH:
            break;
        default:
    }
#endif
}
#endif

int
TclReadlineInitialize(Tcl_Interp* interp, char* historyfile)
{
    rl_readline_name = "tclreadline";
    rl_special_prefixes = "${\"";
    /**
     * default is " \t\n\"\\'`@$><=;|&{("
     * removed "(" <-- arrays
     * removed "{" <-- `${' variables 
     * added "[]"
     */
    rl_basic_word_break_characters = " \t\n\"\\'`@$><=;|&[]";
    rl_completer_quote_characters = "\"";
    /*
    rl_filename_quote_characters
    = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";

    rl_filename_quoting_function
        = (CPFunction*) TclReadlineFilenameQuotingFunction;
    */
    /*
    rl_filename_quoting_desired = 1;
    */

    using_history();
    if (!tclrl_eof_string)
        tclrl_eof_string = strdup("puts {}; exit");

    /*
     * try to read historyfile in home
     * directory. If this failes, this
     * is *not* an error.
     */
    rl_attempted_completion_function = (CPPFunction *) TclReadlineCompletion;
    if (read_history(historyfile)) {
        if (write_history(historyfile)) {
            Tcl_AppendResult (interp, "warning: `",
                historyfile, "' is not writable.", (char*) NULL);
        }
    }
    return TCL_OK;
}

int
blank_line(char* str)
{
    char* ptr;
    for (ptr = str; ptr && *ptr; ptr++) {
        if (!ISWHITE(*ptr))
            return 0;
    }
    return 1;
}

char**
TclReadlineCompletion(char* text, int start, int end)
{
    char** matches = (char**) NULL;
    rl_attempted_completion_over = 0;

#if 0
    fprintf(stderr, "DEBUG> TclReadlineCompletion: text=|%s|\n", text);
    fprintf(stderr, "DEBUG> TclReadlineCompletion: start=|%d|\n", start);
    fprintf(stderr, "DEBUG> TclReadlineCompletion: end=|%d|\n", end);
#endif

#if 0
    char* history_event = (char*) NULL;
    if (text) {
        if ('!' == text[0])
            history_event = strdup(text);
        else if (start && rl_line_buffer[start - 1] == '!' /* for '$' */) {
            int len = strlen(text);
            history_event = strncpy((char*) malloc(sizeof(char) * (len + 1)),
                rl_line_buffer[start - 1], len);
            history_event[len] = '\0'; /* terminate */
        }
    }
    if (history_event)
#endif

    if (text && ('!' == text[0]
            || (start && rl_line_buffer[start - 1] == '!' /* for '$' */))) {
        char* expansion = (char*) NULL;
        int oldlen = strlen(rl_line_buffer);
        int status = history_expand(rl_line_buffer, &expansion);
        if (status >= 1) {
            rl_extend_line_buffer(strlen(expansion) + 1);
            strcpy(rl_line_buffer, expansion);
            rl_end = strlen(expansion);
            rl_point += strlen(expansion) - oldlen;
            FREE(expansion);
            /* rl_redisplay(); */
            /*
             * TODO:
             * because we return 0 == matches,
             * the filename completer will still beep.
            rl_inhibit_completion = 1;
             */
            return matches;
        }
        FREE(expansion);
    }

    if (tclrl_custom_completer) {
        char start_s[BUFSIZ], end_s[BUFSIZ];
        Tcl_Obj* obj;
        Tcl_Obj** objv;
        int objc;
        char* quoted_text = TclReadlineQuote(text, "$[]{}\"");
        char* quoted_rl_line_buffer
            = TclReadlineQuote(rl_line_buffer, "$[]{}\"");
#if 0
        fprintf (stderr, "(TclReadlineCompletion) rl_line_buffer = |%s|\n",
            rl_line_buffer);
        fprintf (stderr, "(TclReadlineCompletion) quoted_rl_line_buffer = |%s|\n",
            quoted_rl_line_buffer);
        fprintf (stderr, "(TclReadlineCompletion) text = |%s|\n", text);
        fprintf (stderr, "(TclReadlineCompletion) quoted_text = |%s|\n",
            quoted_text);
#endif
        sprintf(start_s, "%d", start);
        sprintf(end_s, "%d", end);
        Tcl_ResetResult(tclrl_interp); /* clear result space */
        tclrl_state = Tcl_VarEval(tclrl_interp, tclrl_custom_completer,
            " \"", quoted_text, "\" ", start_s, " ", end_s,
            " \"", quoted_rl_line_buffer, "\"", (char*) NULL);
        FREE(quoted_text);
        FREE(quoted_rl_line_buffer);
        if (TCL_OK != tclrl_state) {
            rl_callback_handler_remove();
            Tcl_AppendResult (tclrl_interp, " `", tclrl_custom_completer,
                " \"", quoted_text, "\" ", start_s, " ", end_s,
                " \"", quoted_rl_line_buffer, "\"' failed.", (char*) NULL);
#if 0
            fprintf(stderr, "\n|%s|\n", Tcl_GetStringResult(tclrl_interp));
#endif
            return matches;
        }
        obj = Tcl_GetObjResult(tclrl_interp);
        Tcl_ListObjGetElements(tclrl_interp, obj, &objc, &objv);
        /* fprintf (stderr, "(TclReadlineCompletion) objc = %d\n", objc); */
        if (objc) {
            int i, length;
            matches = (char**) MALLOC(sizeof(char*) * (objc + 1));
            for (i = 0; i < objc; i++) {
                matches[i] = strdup(Tcl_GetStringFromObj(objv[i], &length));
                if (1 == objc && !strlen(matches[i])) {
                    rl_attempted_completion_over = 1;
                    FREE(matches[i]);
                    FREE(matches);
                    return (char**) NULL;
                }
                /*
                fprintf (stderr, "(TclReadlineCompletion) len[%s]=%d\n",
                    matches[i], strlen(matches[i]));
                */
            }
            matches[i] = (char*) NULL; /* terminate */
        }
        Tcl_ResetResult(tclrl_interp); /* clear result space */
    }

    if (!matches && tclrl_use_builtin_completer) {
        matches = completion_matches(text, TclReadline0generator);
    }
#if 0
    {
        char **ptr;
        for (ptr = matches; ptr && *ptr; ptr++)
            fprintf (stderr, "(TclReadlineCompletion) |%s|\n", *ptr);
    }
#endif
    
    return matches;
}

char*
TclReadline0generator(char* text, int state)
{
    return TclReadlineKnownCommands(text, state, _CMD_GET);
}

char*
TclReadlineKnownCommands(char* text, int state, int mode)
{
    static int len;
    static cmds_t *cmds = (cmds_t *) NULL, *new;
    char* tmp;
    char* args[256];
    int i, argc;
    char** name;

    char* local_line = (char*) NULL;
    int sub;

    
    switch (mode) {
        
        case _CMD_SET:

            new = (cmds_t *) MALLOC(sizeof(cmds_t));
            new->next = (cmds_t *) NULL;

            if (!cmds) {
                cmds = new;
                cmds->prev = new;
            }
            else {
                cmds->prev->next = new;
                cmds->prev = new;
            }

            tmp = strdup(text);
            argc = TclReadlineParse(args, sizeof(args), tmp);

            new->cmd = (char**) MALLOC(sizeof(char*) * (argc + 1));

            for (i = 0; i < argc; i++)
                new->cmd[i] = args[i];

            new->cmd[argc] = (char*) NULL;

            return (char*) NULL;
            break;


        case _CMD_GET:

            local_line = strdup(rl_line_buffer);
            sub = TclReadlineParse(args, sizeof(args), local_line);
            /*
             * fprintf (stderr, "(TclReadlineKnownCommands) state=%d\n", state);
             * fprintf (stderr, "(TclReadlineKnownCommands) text = |%s|\n", text);
             */

            if (0 == sub || (1 == sub && '\0' != text[0])) {
                if (!state) {
                    new = cmds;
                    len = strlen(text);
                }
                while (new && (name = new->cmd)) {
                    new = new->next;
                    if (!strncmp(name[0], text, len))
                        return strdup(name[0]);
                }
                return (char*) NULL;
            } else {

                if (!state) {

                    new = cmds;
                    len = strlen(text);

                    while (new && (name = new->cmd)) {
                        if (!strcmp(name[0], args[0]))
                            break;
                        new = new->next;
                    }

                    if (!new)
                        return (char*) NULL;

                    for (i = 0; new->cmd[i]; i++) /* EMPTY */;

                    if (sub < i && !strncmp(new->cmd[sub], text, len))
                        return strdup(new->cmd[sub]);
                    else
                        return (char*) NULL;

                }
                else
                    return (char*) NULL;

                /* NOTREACHED */
                break;
            }


        default:
            return (char*) NULL;
            break;

    }
    /* NOTREACHED */
}

int
TclReadlineParse(char** args, int maxargs, char* buf)
{
    int nr = 0;

    while (*buf != '\0' && nr < maxargs) {
        /*
         * Strip whitespace.  Use nulls, so
         * that the previous argument is terminated
         * automatically.
         */
        while (ISWHITE(*buf))
            *buf++ = '\0';

        if (!(*buf)) /* don't count the terminating NULL */
            break;

        *args++ = buf;
        nr++;

        while (('\0' != *buf) && !ISWHITE(*buf))
            buf++;
    }

    *args = '\0';
    return nr;
}
