antirez / linenoise

A small self-contained alternative to readline and libedit
BSD 2-Clause "Simplified" License
3.77k stars 665 forks source link

Win32 port #8

Open jgriffiths opened 13 years ago

jgriffiths commented 13 years ago

Hi,

I needed a win32 port. The attached is not particularly clean, but it works fine for me. Compiled with VS 2005. I did it as a separate file since ifdef'ing the existing code got ugly, and it really needs to support raw console mode if its ever going to support emacs key bindings.

I may get time to improve this later in which case I'll update it here. Currently it supports basic line editing and home/end, but no ctrl+key shortcuts, i.e. enough to scratch my immediate itch. If you #define ALT_KEYS you can have ALT+key mappings, but after trying this for a while, my fingers couldn't really adjust to it.

Cheers, Jon

jgriffiths commented 13 years ago

Crap, no attachments. Apologies for the code dump, I'll set up a repo here when I get time.

/* linenoise_win32.c -- Linenoise win32 port.
*
* Modifications copyright 2010, Jon Griffiths <jon_p_griffiths at yahoo dot com>.
* All rights reserved.
* Based on linenoise, copyright 2010, Salvatore Sanfilippo <antirez at gmail dot com>.
* The original linenoise can be found at: http://github.com/antirez/linenoise
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
*   * Redistributions of source code must retain the above copyright notice,
*     this list of conditions and the following disclaimer.
*   * Redistributions in binary form must reproduce the above copyright
*     notice, this list of conditions and the following disclaimer in the
*     documentation and/or other materials provided with the distribution.
*   * Neither the name of Redis nor the names of its contributors may be used
*     to endorse or promote products derived from this software without
*     specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
* Todo list:
* Actually switch to/from raw mode so emacs key combos work.
* Set a console handler to clean up onn exit.
*/
#include <conio.h>
#include <windows.h>
#include <stdio.h>

/* If ALT_KEYS is defined, emacs key combos using ALT instead of CTRL are
* available. At this time, you don't get key repeats when enabled though. */
/* #define ALT_KEYS */

static HANDLE console_in, console_out;

#define LINENOISE_DEFAULT_HISTORY_MAX_LEN 100
#define LINENOISE_MAX_LINE 4096

static int history_max_len = LINENOISE_DEFAULT_HISTORY_MAX_LEN;
static int history_len = 0;
char** history = NULL;

int linenoiseHistoryAdd(const char* line);

static int enableRawMode()
{
if (!console_in)
{
    console_in = GetStdHandle(STD_INPUT_HANDLE);
    console_out = GetStdHandle(STD_OUTPUT_HANDLE);
}
return 0;
}

static void disableRawMode()
{
/* Nothing to do yet */
}

static void output(const char* str,
          size_t      len,
          int         x,
          int         y)
{
COORD pos = { (SHORT)x, (SHORT)y };
WriteConsoleOutputCharacterA(console_out, str, len, pos, 0);
}

static void refreshLine(const char* prompt,
            char*       buf,
            size_t      len,
            size_t      pos,
            size_t      cols)
{
size_t plen = strlen(prompt);

while ((plen + pos) >= cols)
{
    buf++;
    len--;
    pos--;
}
while (plen + len > cols)
{
    len--;
}

CONSOLE_SCREEN_BUFFER_INFO inf = { 0 };
GetConsoleScreenBufferInfo(console_out, &inf);
size_t prompt_len = strlen(prompt);
output(prompt, prompt_len, 0, inf.dwCursorPosition.Y);
output(buf, len, prompt_len, inf.dwCursorPosition.Y);
if (prompt_len + len < (size_t)inf.dwSize.X)
{
    /* Blank to EOL */
    char* tmp = (char*)malloc(inf.dwSize.X - (prompt_len + len));
    memset(tmp, ' ', inf.dwSize.X - (prompt_len + len));
    output(tmp, inf.dwSize.X - (prompt_len + len), len + prompt_len, inf.dwCursorPosition.Y);
    free(tmp);
}
inf.dwCursorPosition.X = (SHORT)(pos + prompt_len);
SetConsoleCursorPosition(console_out, inf.dwCursorPosition);
}

static int linenoisePrompt(char*       buf,
              size_t      buflen,
              const char* prompt)
{
size_t plen = strlen(prompt);
size_t pos = 0;
size_t len = 0;
int history_index = 0;
#ifdef ALT_KEYS
unsigned char last_down = 0;
#endif
buf[0] = '\0';
buflen--; /* Make sure there is always space for the nulterm */

/* The latest history entry is always our current buffer, that
* initially is just an empty string. */
linenoiseHistoryAdd("");

CONSOLE_SCREEN_BUFFER_INFO inf = { 0 };
GetConsoleScreenBufferInfo(console_out, &inf);
size_t cols = inf.dwSize.X;
output(prompt, plen, 0, inf.dwCursorPosition.Y);
inf.dwCursorPosition.X = (SHORT)plen;
SetConsoleCursorPosition(console_out, inf.dwCursorPosition);

for ( ; ; )
{
    INPUT_RECORD rec;
    DWORD count;
    ReadConsoleInputA(console_in, &rec, 1, &count);
    if (rec.EventType != KEY_EVENT)
    continue;
#ifdef ALT_KEYS
    if (rec.Event.KeyEvent.bKeyDown)
    {
    last_down = rec.Event.KeyEvent.uChar.AsciiChar;
    continue;
    }
#else
    if (!rec.Event.KeyEvent.bKeyDown)
    {
    continue;
    }
#endif
    switch (rec.Event.KeyEvent.wVirtualKeyCode)
    {
    case VK_RETURN:    /* enter */
    history_len--;
    free(history[history_len]);
    return (int)len;
    case VK_BACK:   /* backspace */
#ifdef ALT_KEYS
backspace:
#endif
    if (pos > 0 && len > 0)
    {
        memmove(buf + pos - 1, buf + pos, len - pos);
        pos--;
        len--;
        buf[len] = '\0';
        refreshLine(prompt, buf, len, pos, cols);
    }
    break;
    case VK_LEFT:
#ifdef ALT_KEYS
left_arrow:
#endif
    /* left arrow */
    if (pos > 0)
    {
        pos--;
        refreshLine(prompt, buf, len, pos, cols);
    }
    break;
    case VK_RIGHT:
#ifdef ALT_KEYS
right_arrow:
#endif
    /* right arrow */
    if (pos != len)
    {
        pos++;
        refreshLine(prompt, buf, len, pos, cols);
    }
    break;
    case VK_UP:
    case VK_DOWN:
#ifdef ALT_KEYS
up_down_arrow:
#endif
    /* up and down arrow: history */
    if (history_len > 1)
    {
        /* Update the current history entry before to
        * overwrite it with tne next one. */
        free(history[history_len - 1 - history_index]);
        history[history_len - 1 - history_index] = _strdup(buf);
        /* Show the new entry */
        history_index += (rec.Event.KeyEvent.wVirtualKeyCode == VK_UP) ? 1 : -1;
        if (history_index < 0)
        {
        history_index = 0;
        break;
        }
        else if (history_index >= history_len)
        {
        history_index = history_len - 1;
        break;
        }
        strncpy(buf, history[history_len - 1 - history_index], buflen);
        buf[buflen] = '\0';
        len = pos = strlen(buf);
        refreshLine(prompt, buf, len, pos, cols);
    }
    break;
    case VK_DELETE:
    /* delete */
    if (len > 0 && pos < len)
    {
        memmove(buf + pos, buf + pos + 1, len - pos - 1);
        len--;
        buf[len] = '\0';
        refreshLine(prompt, buf, len, pos, cols);
    }
    break;
    case VK_HOME: /* Ctrl+a, go to the start of the line */
#ifdef ALT_KEYS
home:
#endif
    pos = 0;
    refreshLine(prompt, buf, len, pos, cols);
    break;
    case VK_END: /* ctrl+e, go to the end of the line */
#ifdef ALT_KEYS
end:
#endif
    pos = len;
    refreshLine(prompt, buf, len, pos, cols);
    break;
    default:
#ifdef ALT_KEYS
    /* Use alt instead of CTRL since windows eats CTRL+char combos */
    if (rec.Event.KeyEvent.dwControlKeyState & (LEFT_ALT_PRESSED | RIGHT_ALT_PRESSED))
    {
        switch (last_down)
        {
        case 'a': /* ctrl-t */
        goto home;
        case 'e': /* ctrl-t */
        goto end;
        case 't': /* ctrl-t */
        if (pos > 0 && pos < len)
        {
            int aux = buf[pos - 1];
            buf[pos - 1] = buf[pos];
            buf[pos] = aux;
            if (pos != len - 1)
            pos++;
            refreshLine(prompt, buf, len, pos, cols);
        }
        break;
        case 'h': /* ctrl-h */
        goto backspace;
        case 'b': /* ctrl-b */
        goto left_arrow;
        case 'f': /* ctrl-f */
        goto right_arrow;
        case 'p': /* ctrl-p */
        rec.Event.KeyEvent.wVirtualKeyCode = VK_UP;
        goto up_down_arrow;
        case 'n': /* ctrl-n */
        rec.Event.KeyEvent.wVirtualKeyCode = VK_DOWN;
        goto up_down_arrow;
        case 'u': /* Ctrl+u, delete the whole line. */
        buf[0] = '\0';
        pos = len = 0;
        refreshLine(prompt, buf, len, pos, cols);
        break;
        case 'k': /* Ctrl+k, delete from current to end of line. */
        buf[pos] = '\0';
        len = pos;
        refreshLine(prompt, buf, len, pos, cols);
        break;
        }
        continue;
    }
#endif /* ALT_KEYS */
    if (rec.Event.KeyEvent.uChar.AsciiChar < ' ' ||
        rec.Event.KeyEvent.uChar.AsciiChar > '~')
        continue;

    if (len < buflen)
    {
        if (len != pos)
        memmove(buf + pos + 1, buf + pos, len - pos);
        buf[pos] = rec.Event.KeyEvent.uChar.AsciiChar;
        len++;
        pos++;
        buf[len] = '\0';
        refreshLine(prompt, buf, len, pos, cols);
    }
    break;
    }
}
}

static int linenoiseRaw(char*       buf,
            size_t      buflen,
            const char* prompt)
{
int count = -1;

if (buflen != 0)
{
    if (enableRawMode() == -1)
    return -1;
    count = linenoisePrompt(buf, buflen, prompt);
    disableRawMode();
    printf("\n");
}
return count;
}

char* linenoise(const char* prompt)
{
char buf[LINENOISE_MAX_LINE];
int count = linenoiseRaw(buf, LINENOISE_MAX_LINE, prompt);
if (count == -1)
    return NULL;
return _strdup(buf);
}

/* Using a circular buffer is smarter, but a bit more complex to handle. */
int linenoiseHistoryAdd(const char* line)
{
char* linecopy;

if (history_max_len == 0)
    return 0;
if (history == NULL)
{
    history = (char**)malloc(sizeof(char*) * history_max_len);
    if (history == NULL)
    return 0;
    memset(history, 0, (sizeof(char*) * history_max_len));
}
linecopy = _strdup(line);
if (!linecopy)
    return 0;
if (history_len == history_max_len)
{
    free(history[0]);
    memmove(history, history + 1, sizeof(char*) * (history_max_len - 1));
    history_len--;
}
history[history_len] = linecopy;
history_len++;
return 1;
}

int linenoiseHistorySetMaxLen(int len)
{
char** new_history;

if (len < 1)
    return 0;
if (history)
{
    int tocopy = history_len;

    new_history = (char**)malloc(sizeof(char*) * len);
    if (new_history == NULL)
    return 0;
    if (len < tocopy)
    tocopy = len;
    memcpy(new_history, history + (history_max_len - tocopy), sizeof(char*) * tocopy);
    free(history);
    history = new_history;
}
history_max_len = len;
if (history_len > history_max_len)
    history_len = history_max_len;
return 1;
}

/* Save the history in the specified file. On success 0 is returned
* otherwise -1 is returned. */
int linenoiseHistorySave(const char* filename)
{
FILE* fp = fopen(filename, "w");
int j;

if (fp == NULL)
    return -1;
for (j = 0; j < history_len; j++)
    fprintf(fp, "%s\n", history[j]);
fclose(fp);
return 0;
}

/* Load the history from the specified file. If the file does not exist
* zero is returned and no operation is performed.
*
* If the file exists and the operation succeeded 0 is returned, otherwise
* on error -1 is returned. */
int linenoiseHistoryLoad(const char* filename)
{
FILE* fp = fopen(filename, "r");
char buf[LINENOISE_MAX_LINE];

if (fp == NULL)
    return -1;

while (fgets(buf, LINENOISE_MAX_LINE, fp) != NULL)
{
    char* p;

    p = strchr(buf, '\r');
    if (!p)
    p = strchr(buf, '\n');
    if (p)
    *p = '\0';
    linenoiseHistoryAdd(buf);
}
fclose(fp);
return 0;
}
msteveb commented 13 years ago

FWIW, I have integrated win32 console support into my fork of linenoise (see https://github.com/msteveb/linenoise/commit/85f6003a0ee9d443966c43f99403bb319c555cd5)

jgriffiths commented 13 years ago

Cool stuff. I see you are listed as the author, could you put an ack in the comments?

Cheers Jon


From: Steve Bennett reply@reply.github.com To: jgriffiths jon_p_griffiths@yahoo.com Sent: Wednesday, September 14, 2011 1:16 PM Subject: Re: [linenoise] Win32 port (#8)

FWIW, I have integrated win32 console support into my fork of linenoise (see https://github.com/msteveb/linenoise/commit/85f6003a0ee9d443966c43f99403bb319c555cd5)

Reply to this email directly or view it on GitHub: https://github.com/antirez/linenoise/issues/8#issuecomment-2088704

msteveb commented 13 years ago

Sure. See the latest commit in my fork.

jgriffiths commented 13 years ago

Brilliant, cheers mate :)

J


From: Steve Bennett reply@reply.github.com To: jgriffiths jon_p_griffiths@yahoo.com Sent: Thursday, September 15, 2011 3:19 PM Subject: Re: [linenoise] Win32 port (#8)

Sure. See the latest commit in my fork.

Reply to this email directly or view it on GitHub: https://github.com/antirez/linenoise/issues/8#issuecomment-2100627

MattWindsor91 commented 9 years ago

Sorry for gravedigging, but did anything ever come of this? I was looking at linenoise (via rust-linenoise), but the current master has no win32 support by the looks of it, and I've been looking for something readline-esque that works on bofh *nix and win32 =(

mcfriend99 commented 2 years ago

Is there ever going to be work to make this part of the project itself??