From 63df918482ee7c3f672ec5db7518fa2057cdf4ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emysl=20Janouch?= Date: Thu, 24 Dec 2015 15:45:49 +0100 Subject: [PATCH] Rewrite the input layer Now we also support libedit for the backend. --- CMakeLists.txt | 19 +- README.adoc | 3 +- config.h.in | 3 + json-rpc-shell.c | 827 +++++++++++++++++++++++++++++++++++++++++------ 4 files changed, 749 insertions(+), 103 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index a706b0a..1e2f51d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,10 @@ project (json-rpc-shell C) cmake_minimum_required (VERSION 2.8.5) +# Options +option (WANT_READLINE "Use GNU Readline for the UI (better)" ON) +option (WANT_LIBEDIT "Use BSD libedit for the UI" OFF) + # Moar warnings if ("${CMAKE_C_COMPILER_ID}" MATCHES "GNU" OR CMAKE_COMPILER_IS_GNUC) # -Wunused-function is pretty annoying here, as everything is static @@ -28,7 +32,7 @@ find_package (LibEV REQUIRED) pkg_check_modules (ncursesw ncursesw) set (project_libraries ${dependencies_LIBRARIES} - ${libssl_LIBRARIES} ${LIBEV_LIBRARIES} readline) + ${libssl_LIBRARIES} ${LIBEV_LIBRARIES}) include_directories (${dependencies_INCLUDE_DIRS} ${libssl_INCLUDE_DIRS} ${LIBEV_INCLUDE_DIRS}) @@ -42,7 +46,20 @@ else (CURSES_FOUND) message (SEND_ERROR "Curses not found") endif (ncursesw_FOUND) +if ((WANT_READLINE AND WANT_LIBEDIT) OR (NOT WANT_READLINE AND NOT WANT_LIBEDIT)) + message (SEND_ERROR "You have to choose either GNU Readline or libedit") +elseif (WANT_READLINE) + list (APPEND project_libraries readline) +elseif (WANT_LIBEDIT) + pkg_check_modules (libedit REQUIRED libedit) + list (APPEND project_libraries ${libedit_LIBRARIES}) + include_directories (${libedit_INCLUDE_DIRS}) +endif ((WANT_READLINE AND WANT_LIBEDIT) OR (NOT WANT_READLINE AND NOT WANT_LIBEDIT)) + # Generate a configuration file +set (HAVE_READLINE "${WANT_READLINE}") +set (HAVE_EDITLINE "${WANT_LIBEDIT}") + configure_file (${PROJECT_SOURCE_DIR}/config.h.in ${PROJECT_BINARY_DIR}/config.h) include_directories (${PROJECT_BINARY_DIR}) diff --git a/README.adoc b/README.adoc index 4856080..dcf8128 100644 --- a/README.adoc +++ b/README.adoc @@ -29,7 +29,8 @@ Building and Running -------------------- Build dependencies: CMake, pkg-config, help2man, liberty (included), http-parser (included) + -Runtime dependencies: libev, Jansson, cURL, readline, openssl +Runtime dependencies: libev, Jansson, cURL, openssl, + readline or libedit >= 2013-07-12, $ git clone --recursive https://github.com/pjanouch/json-rpc-shell.git $ mkdir json-rpc-shell/build diff --git a/config.h.in b/config.h.in index 89cd306..b7b4101 100644 --- a/config.h.in +++ b/config.h.in @@ -4,5 +4,8 @@ #define PROGRAM_NAME "${PROJECT_NAME}" #define PROGRAM_VERSION "${project_VERSION}" +#cmakedefine HAVE_READLINE +#cmakedefine HAVE_EDITLINE + #endif // ! CONFIG_H diff --git a/json-rpc-shell.c b/json-rpc-shell.c index a1b3ae4..16bc31e 100644 --- a/json-rpc-shell.c +++ b/json-rpc-shell.c @@ -56,8 +56,6 @@ enum #include #include -#include -#include #include #include #include @@ -80,6 +78,7 @@ static struct bool stdout_is_tty; ///< `stdout' is a terminal bool stderr_is_tty; ///< `stderr' is a terminal + struct termios termios; ///< Terminal attributes char *color_set[8]; ///< Codes to set the foreground colour } g_terminal; @@ -97,6 +96,9 @@ init_terminal (void) if (tty_fd == -1 || setupterm (NULL, tty_fd, &err) == ERR) return false; + if (tcgetattr (tty_fd, &g_terminal.termios)) + return false; + // Make sure all terminal features used by us are supported if (!set_a_foreground || !enter_bold_mode || !exit_attribute_mode) { @@ -122,6 +124,672 @@ free_terminal (void) del_curterm (cur_term); } +// --- User interface ---------------------------------------------------------- + +// Not trying to do anything crazy here like switchable buffers. +// Not trying to be too universal here either, it's not going to be reusable. + +struct input +{ + struct input_vtable *vtable; ///< Virtual methods + void *user_data; ///< User data for callbacks + + /// Process a single line input by the user + void (*on_input) (char *line, void *user_data); +}; + +struct input_vtable +{ + /// Start the interface under the given program name + void (*start) (struct input *input, const char *program_name); + /// Stop the interface + void (*stop) (struct input *input); + /// Destroy the object + void (*destroy) (struct input *input); + + /// Hide the prompt if shown + void (*hide) (struct input *input); + /// Show the prompt if hidden + void (*show) (struct input *input); + /// Change the prompt string; takes ownership + void (*set_prompt) (struct input *input, char *prompt); + /// Ring the terminal bell + void (*ding) (struct input *input); + + /// Load history from file + bool (*load_history) (struct input *input, const char *filename, + struct error **e); + /// Save history to file + bool (*save_history) (struct input *input, const char *filename, + struct error **e); + + /// Handle terminal resize + void (*on_terminal_resized) (struct input *input); + /// Handle terminal input + void (*on_tty_readable) (struct input *input); +}; + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +#ifdef HAVE_READLINE + +#include +#include + +#define INPUT_START_IGNORE RL_PROMPT_START_IGNORE +#define INPUT_END_IGNORE RL_PROMPT_END_IGNORE + +struct input_rl +{ + struct input super; ///< Parent class + + bool active; ///< Interface has been started + char *prompt; ///< The prompt we use + int prompt_shown; ///< Whether the prompt is shown now + + char *saved_line; ///< Saved line content + int saved_point; ///< Saved cursor position + int saved_mark; ///< Saved mark +}; + +/// Unfortunately Readline cannot pass us any pointer value in its callbacks +/// that would eliminate the need to use global variables ourselves +static struct input_rl *g_input_rl; + +static void +input_rl_erase (void) +{ + rl_set_prompt (""); + rl_replace_line ("", false); + rl_redisplay (); +} + +static void +input_rl_on_input (char *line) +{ + struct input_rl *self = g_input_rl; + + // The prompt should always be visible at the moment we process input keys; + // confirming it de facto hides it because we move onto a new line + if (line) + self->prompt_shown = 0; + if (line && *line) + add_history (line); + + self->super.on_input (line, self->super.user_data); + free (line); + + // Readline automatically redisplays the prompt after we're done here; + // we could have actually hidden it by now in preparation of a quit though + if (line) + self->prompt_shown++; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static void +input_rl_start (struct input *input, const char *program_name) +{ + struct input_rl *self = (struct input_rl *) input; + using_history (); + // This can cause memory leaks, or maybe even a segfault. Funny, eh? + stifle_history (HISTORY_LIMIT); + + const char *slash = strrchr (program_name, '/'); + rl_readline_name = slash ? ++slash : program_name; + rl_catch_sigwinch = false; + + hard_assert (self->prompt != NULL); + rl_callback_handler_install (self->prompt, input_rl_on_input); + + self->active = true; + self->prompt_shown = 1; + g_input_rl = self; +} + +static void +input_rl_stop (struct input *input) +{ + struct input_rl *self = (struct input_rl *) input; + if (self->prompt_shown > 0) + input_rl_erase (); + + // This is okay so long as we're not called from within readline + rl_callback_handler_remove (); + + self->active = false; + self->prompt_shown = 0; + g_input_rl = NULL; +} + +static void +input_rl_destroy (struct input *input) +{ + struct input_rl *self = (struct input_rl *) input; + + if (self->active) + input_rl_stop (input); + + free (self->saved_line); + free (self->prompt); + free (self); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static void +input_rl_hide (struct input *input) +{ + struct input_rl *self = (struct input_rl *) input; + if (!self->active || self->prompt_shown-- < 1) + return; + + hard_assert (!self->saved_line); + + self->saved_point = rl_point; + self->saved_mark = rl_mark; + self->saved_line = rl_copy_text (0, rl_end); + + input_rl_erase (); +} + +static void +input_rl_show (struct input *input) +{ + struct input_rl *self = (struct input_rl *) input; + if (!self->active || ++self->prompt_shown < 1) + return; + + hard_assert (self->saved_line); + + rl_set_prompt (self->prompt); + rl_replace_line (self->saved_line, false); + rl_point = self->saved_point; + rl_mark = self->saved_mark; + free (self->saved_line); + self->saved_line = NULL; + + rl_redisplay (); +} + +static void +input_rl_set_prompt (struct input *input, char *prompt) +{ + struct input_rl *self = (struct input_rl *) input; + free (self->prompt); + self->prompt = prompt; + + if (!self->active) + return; + + // First reset the prompt to work around a bug in readline + rl_set_prompt (""); + if (self->prompt_shown > 0) + rl_redisplay (); + + rl_set_prompt (self->prompt); + if (self->prompt_shown > 0) + rl_redisplay (); +} + +static void +input_rl_ding (struct input *input) +{ + (void) input; + rl_ding (); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static bool +input_rl_load_history (struct input *input, const char *filename, + struct error **e) +{ + (void) input; + + if (!(errno = read_history (filename))) + return true; + + error_set (e, "%s", strerror (errno)); + return false; +} + +static bool +input_rl_save_history (struct input *input, const char *filename, + struct error **e) +{ + (void) input; + + if (!(errno = write_history (filename))) + return true; + + error_set (e, "%s", strerror (errno)); + return false; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static void +input_rl_on_terminal_resized (struct input *input) +{ + // This fucks up big time on terminals with automatic wrapping such as + // rxvt-unicode or newer VTE when the current line overflows, however we + // can't do much about that + (void) input; + rl_resize_terminal (); +} + +static void +input_rl_on_tty_readable (struct input *input) +{ + (void) input; + rl_callback_read_char (); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static struct input_vtable input_rl_vtable = +{ + .start = input_rl_start, + .stop = input_rl_stop, + .destroy = input_rl_destroy, + + .hide = input_rl_hide, + .show = input_rl_show, + .set_prompt = input_rl_set_prompt, + .ding = input_rl_ding, + + .load_history = input_rl_load_history, + .save_history = input_rl_save_history, + + .on_terminal_resized = input_rl_on_terminal_resized, + .on_tty_readable = input_rl_on_tty_readable, +}; + +static struct input * +input_rl_new (void) +{ + struct input_rl *self = xcalloc (1, sizeof *self); + self->super.vtable = &input_rl_vtable; + return &self->super; +} + +#define input_new input_rl_new +#endif // HAVE_READLINE + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +#ifdef HAVE_EDITLINE + +#include + +#define INPUT_START_IGNORE '\x01' +#define INPUT_END_IGNORE '\x01' + +struct input_el +{ + struct input super; ///< Parent class + + EditLine *editline; ///< The EditLine object + HistoryW *history; ///< The history object + char *entered_line; ///< Buffers the entered line + + bool active; ///< Interface has been started + char *prompt; ///< The prompt we use + int prompt_shown; ///< Whether the prompt is shown now + + wchar_t *saved_line; ///< Saved line content + int saved_point; ///< Saved cursor position + int saved_len; ///< Saved line length +}; + +static char * +input_el_wcstombs (const wchar_t *s) +{ + size_t len = wcstombs (NULL, s, 0); + if (len++ == (size_t) -1) + return NULL; + + char *mb = xmalloc (len); + mb[wcstombs (mb, s, len)] = 0; + return mb; +} + +static int +input_el_get_termios (int character, int fallback) +{ + if (!g_terminal.initialized) + return fallback; + + cc_t value = g_terminal.termios.c_cc[character]; + if (value == _POSIX_VDISABLE) + return fallback; + return value; +} + +static void +input_el_redisplay (struct input_el *self) +{ + char x[] = { input_el_get_termios (VREPRINT, 'R' - 0x40), 0 }; + el_push (self->editline, x); + + // We have to do this or it gets stuck and nothing is done + (void) el_gets (self->editline, NULL); +} + +static char * +input_el_make_prompt (EditLine *editline) +{ + struct input_el *self; + el_get (editline, EL_CLIENTDATA, &self); + return self->prompt ? self->prompt : ""; +} + +static char * +input_el_make_empty_prompt (EditLine *editline) +{ + (void) editline; + return ""; +} + +static void +input_el_erase (struct input_el *self) +{ + const LineInfoW *info = el_wline (self->editline); + int len = info->lastchar - info->buffer; + int point = info->cursor - info->buffer; + el_cursor (self->editline, len - point); + el_wdeletestr (self->editline, len); + + el_set (self->editline, EL_PROMPT, input_el_make_empty_prompt); + input_el_redisplay (self); +} + +static unsigned char +input_el_on_return (EditLine *editline, int key) +{ + (void) key; + + struct input_el *self; + el_get (editline, EL_CLIENTDATA, &self); + + const LineInfoW *info = el_wline (editline); + int len = info->lastchar - info->buffer; + int point = info->cursor - info->buffer; + + wchar_t *line = calloc (sizeof *info->buffer, len + 1); + memcpy (line, info->buffer, sizeof *info->buffer * len); + + if (*line) + { + HistEventW ev; + history_w (self->history, &ev, H_ENTER, line); + } + free (line); + + // Convert to a multibyte string and store it for later + const LineInfo *info_mb = el_line (editline); + self->entered_line = xstrndup + (info_mb->buffer, info_mb->lastchar - info_mb->buffer); + + // Now we need to force editline to actually print the newline + el_cursor (editline, len++ - point); + el_insertstr (editline, "\n"); + input_el_redisplay (self); + + // Finally we need to discard the old line's contents + el_wdeletestr (editline, len); + return CC_NEWLINE; +} + +static void +input_el_install_prompt (struct input_el *self) +{ + // XXX: the ignore doesn't quite work, see https://gnats.netbsd.org/47539 + el_set (self->editline, EL_PROMPT_ESC, + input_el_make_prompt, INPUT_START_IGNORE); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static void +input_el_start (struct input *input, const char *program_name) +{ + struct input_el *self = (struct input_el *) input; + self->editline = el_init (program_name, stdin, stdout, stderr); + el_set (self->editline, EL_CLIENTDATA, self); + input_el_install_prompt (self); + el_set (self->editline, EL_SIGNAL, false); + el_set (self->editline, EL_UNBUFFERED, true); + el_set (self->editline, EL_EDITOR, "emacs"); + el_wset (self->editline, EL_HIST, history_w, self->history); + + // No, editline, it's not supposed to kill the entire line + el_set (self->editline, EL_BIND, "^w", "ed-delete-prev-word", NULL); + // Just what are you doing? + el_set (self->editline, EL_BIND, "^u", "vi-kill-line-prev", NULL); + + // It's probably better to handle this ourselves + el_set (self->editline, EL_ADDFN, + "send-line", "Send line", input_el_on_return); + el_set (self->editline, EL_BIND, "\n", "send-line", NULL); + + // Source the user's defaults file + el_source (self->editline, NULL); + + self->active = true; + self->prompt_shown = 1; +} + +static void +input_el_stop (struct input *input) +{ + struct input_el *self = (struct input_el *) input; + if (self->prompt_shown > 0) + input_el_erase (self); + + el_end (self->editline); + self->editline = NULL; + + self->active = false; + self->prompt_shown = 0; +} + +static void +input_el_destroy (struct input *input) +{ + struct input_el *self = (struct input_el *) input; + + if (self->active) + input_el_stop (input); + + history_wend (self->history); + + free (self->saved_line); + free (self->prompt); + free (self); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static void +input_el_hide (struct input *input) +{ + struct input_el *self = (struct input_el *) input; + if (!self->active || self->prompt_shown-- < 1) + return; + + hard_assert (!self->saved_line); + + const LineInfoW *info = el_wline (self->editline); + int len = info->lastchar - info->buffer; + int point = info->cursor - info->buffer; + + wchar_t *line = calloc (sizeof *info->buffer, len + 1); + memcpy (line, info->buffer, sizeof *info->buffer * len); + el_cursor (self->editline, len - point); + el_wdeletestr (self->editline, len); + + self->saved_line = line; + self->saved_point = point; + self->saved_len = len; + + input_el_erase (self); +} + +static void +input_el_show (struct input *input) +{ + struct input_el *self = (struct input_el *) input; + if (!self->active || ++self->prompt_shown < 1) + return; + + hard_assert (self->saved_line); + + el_winsertstr (self->editline, self->saved_line); + el_cursor (self->editline, + -(self->saved_len - self->saved_point)); + free (self->saved_line); + self->saved_line = NULL; + + input_el_install_prompt (self); + input_el_redisplay (self); +} + +static void +input_el_set_prompt (struct input *input, char *prompt) +{ + struct input_el *self = (struct input_el *) input; + free (self->prompt); + self->prompt = prompt; + + if (self->prompt_shown > 0) + input_el_redisplay (self); +} + +static void +input_el_ding (struct input *input) +{ + (void) input; + + const char *ding = bell ? bell : "\a"; + write (STDOUT_FILENO, ding, strlen (ding)); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static bool +input_el_load_history (struct input *input, const char *filename, + struct error **e) +{ + struct input_el *self = (struct input_el *) input; + + HistEventW ev; + if (history_w (self->history, &ev, H_LOAD, filename) != -1) + return true; + + char *error = input_el_wcstombs (ev.str); + error_set (e, "%s", error); + free (error); + return false; +} + +static bool +input_el_save_history (struct input *input, const char *filename, + struct error **e) +{ + struct input_el *self = (struct input_el *) input; + + HistEventW ev; + if (history_w (self->history, &ev, H_SAVE, filename) != -1) + return true; + + char *error = input_el_wcstombs (ev.str); + error_set (e, "%s", error); + free (error); + return false; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static void +input_el_on_terminal_resized (struct input *input) +{ + struct input_el *self = (struct input_el *) input; + el_resize (self->editline); +} + +static void +input_el_on_tty_readable (struct input *input) +{ + // We bind the return key to process it how we need to + struct input_el *self = (struct input_el *) input; + + // el_gets() with EL_UNBUFFERED doesn't work with UTF-8, + // we must use the wide-character interface + int count = 0; + const wchar_t *buf = el_wgets (self->editline, &count); + + // Process data from our newline handler (async-friendly handling) + if (self->entered_line) + { + // We can't have anything try to hide the old prompt with the appended + // newline, it needs to stay where it is and as it is + self->prompt_shown = 0; + + self->super.on_input (self->entered_line, self->super.user_data); + free (self->entered_line); + self->entered_line = NULL; + + // Forbid editline from trying to erase the old prompt (or worse) + // and let it redisplay the prompt in its clean state + el_set (self->editline, EL_REFRESH); + self->prompt_shown = 1; + } + + if (count == 1 && buf[0] == ('D' - 0x40) /* hardcoded VEOF in editline */) + { + el_deletestr (self->editline, 1); + input_el_redisplay (self); + self->super.on_input (NULL, self->super.user_data); + } +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static struct input_vtable input_el_vtable = +{ + .start = input_el_start, + .stop = input_el_stop, + .destroy = input_el_destroy, + + .hide = input_el_hide, + .show = input_el_show, + .set_prompt = input_el_set_prompt, + .ding = input_el_ding, + + .load_history = input_el_load_history, + .save_history = input_el_save_history, + + .on_terminal_resized = input_el_on_terminal_resized, + .on_tty_readable = input_el_on_tty_readable, +}; + +static struct input * +input_el_new (void) +{ + struct input_el *self = xcalloc (1, sizeof *self); + self->super.vtable = &input_el_vtable; + + HistEventW ev; + self->history = history_winit (); + history_w (self->history, &ev, H_SETSIZE, HISTORY_LIMIT); + return &self->super; +} + +#define input_new input_el_new +#endif // HAVE_EDITLINE + // --- Main program ------------------------------------------------------------ // HTTP/S and WS/S require significantly different handling. While for HTTP we @@ -251,14 +919,15 @@ enum color_mode static struct app_context { + struct input *input; ///< Input interface + char *attrs_defaults[ATTR_COUNT]; ///< Default terminal attributes + char *attrs[ATTR_COUNT]; ///< Terminal attributes + struct backend_iface *backend; ///< Our current backend struct ws_context ws; ///< WebSockets backend data struct curl_context curl; ///< cURL backend data - char *attrs_defaults[ATTR_COUNT]; ///< Default terminal attributes - char *attrs[ATTR_COUNT]; ///< Terminal attributes - struct config config; ///< Program configuration enum color_mode color_mode; ///< Colour output mode bool pretty_print; ///< Whether to pretty print @@ -270,9 +939,6 @@ static struct app_context iconv_t term_to_utf8; ///< Terminal encoding to UTF-8 iconv_t term_from_utf8; ///< UTF-8 to terminal encoding - - char *readline_prompt; ///< The prompt we use for readline - bool readline_prompt_shown; ///< Whether the prompt is shown now } g_ctx; @@ -468,34 +1134,13 @@ log_message_attributed (void *user_data, const char *quote, const char *fmt, va_list ap) { FILE *stream = stderr; - - // GNU readline is a huge piece of total crap; it seems that we must do - // these incredible shenanigans in order to intersperse readline output - // with asynchronous status messages - char *saved_line; - int saved_point; - - if (g_ctx.readline_prompt_shown) - { - saved_point = rl_point; - saved_line = rl_copy_text (0, rl_end); - rl_set_prompt (""); - rl_replace_line ("", 0); - rl_redisplay (); - } + g_ctx.input->vtable->hide (g_ctx.input); print_attributed (&g_ctx, stream, (intptr_t) user_data, "%s", quote); vprint_attributed (&g_ctx, stream, (intptr_t) user_data, fmt, ap); fputs ("\n", stream); - if (g_ctx.readline_prompt_shown) - { - rl_set_prompt (g_ctx.readline_prompt); - rl_replace_line (saved_line, 0); - rl_point = saved_point; - rl_redisplay (); - free (saved_line); - } + g_ctx.input->vtable->show (g_ctx.input); } static void @@ -1659,6 +2304,18 @@ static struct backend_iface g_backend_curl = // --- Main program ------------------------------------------------------------ +static void +quit (struct app_context *ctx) +{ + if (ctx->backend->on_quit) + ctx->backend->on_quit (ctx); + + ev_break (EV_DEFAULT_ EVBREAK_ALL); + ctx->input->vtable->hide (ctx->input); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + #define PARSE_FAIL(...) \ BLOCK_START \ print_error (__VA_ARGS__); \ @@ -1842,8 +2499,15 @@ fail: } static void -process_input (struct app_context *ctx, char *user_input) +process_input (char *user_input, void *user_data) { + struct app_context *ctx = user_data; + if (!user_input) + { + quit (ctx); + return; + } + char *input; size_t len; @@ -1940,68 +2604,28 @@ on_winch (EV_P_ ev_signal *handle, int revents) (void) handle; (void) revents; - // This fucks up big time on terminals with automatic wrapping such as - // rxvt-unicode or newer VTE when the current line overflows, however we - // can't do much about that - rl_resize_terminal (); -} - -static void -quit (struct app_context *ctx) -{ - if (ctx->backend->on_quit) - ctx->backend->on_quit (ctx); - - ev_break (EV_DEFAULT_ EVBREAK_ALL); + struct app_context *ctx = ev_userdata (loop); + ctx->input->vtable->on_terminal_resized (ctx->input); } static void on_terminated (EV_P_ ev_signal *handle, int revents) { - (void) loop; (void) handle; (void) revents; - quit (&g_ctx); -} - -static void -on_readline_input (char *line) -{ - // Otherwise the prompt is shown at all times - // Stupid readline forces us to use a global variable - g_ctx.readline_prompt_shown = false; - - if (!line) - { - quit (&g_ctx); - - // We must do this here, or the prompt gets printed twice. *shrug* - rl_callback_handler_remove (); - - // Note that we don't set "readline_prompt_shown" back to true. - // This is so that we can safely do rl_callback_handler_remove when - // the program is terminated in an unusual manner (other than ^D). - return; - } - - if (*line) - add_history (line); - - process_input (&g_ctx, line); - free (line); - - g_ctx.readline_prompt_shown = true; + struct app_context *ctx = ev_userdata (loop); + quit (ctx); } static void on_tty_readable (EV_P_ ev_io *handle, int revents) { - (void) loop; (void) handle; + struct app_context *ctx = ev_userdata (loop); if (revents & EV_READ) - rl_callback_read_char (); + ctx->input->vtable->on_tty_readable (ctx->input); } static void @@ -2158,23 +2782,26 @@ main (int argc, char *argv[]) data_home = xstrdup_printf ("%s/.local/share", home); } - using_history (); - stifle_history (HISTORY_LIMIT); + g_ctx.input = input_new (); + g_ctx.input->user_data = &g_ctx; + g_ctx.input->on_input = process_input; char *history_path = xstrdup_printf ("%s/" PROGRAM_NAME "/history", data_home); - (void) read_history (history_path); + (void) g_ctx.input->vtable->load_history (g_ctx.input, history_path, NULL); if (!get_attribute_printer (stdout)) - g_ctx.readline_prompt = xstrdup_printf ("json-rpc> "); + g_ctx.input->vtable->set_prompt (g_ctx.input, + xstrdup_printf ("json-rpc> ")); else { // XXX: to be completely correct, we should use tputs, but we cannot - g_ctx.readline_prompt = xstrdup_printf ("%c%s%cjson-rpc> %c%s%c", - RL_PROMPT_START_IGNORE, g_ctx.attrs[ATTR_PROMPT], - RL_PROMPT_END_IGNORE, - RL_PROMPT_START_IGNORE, g_ctx.attrs[ATTR_RESET], - RL_PROMPT_END_IGNORE); + g_ctx.input->vtable->set_prompt (g_ctx.input, + xstrdup_printf ("%c%s%cjson-rpc> %c%s%c", + INPUT_START_IGNORE, g_ctx.attrs[ATTR_PROMPT], + INPUT_END_IGNORE, + INPUT_START_IGNORE, g_ctx.attrs[ATTR_RESET], + INPUT_END_IGNORE)); } // So that if the remote end closes the connection, attempts to write to @@ -2202,35 +2829,33 @@ main (int argc, char *argv[]) ev_io_init (&tty_watcher, on_tty_readable, STDIN_FILENO, EV_READ); ev_io_start (EV_DEFAULT_ &tty_watcher); - // readline 6.3 doesn't immediately redraw the terminal upon reception - // of SIGWINCH, so we must run it in an event loop to remediate that - rl_catch_sigwinch = false; - g_ctx.readline_prompt_shown = true; - rl_callback_handler_install (g_ctx.readline_prompt, on_readline_input); + g_ctx.input->vtable->start (g_ctx.input, PROGRAM_NAME); + ev_set_userdata (loop, &g_ctx); ev_run (loop, 0); - if (g_ctx.readline_prompt_shown) - rl_callback_handler_remove (); - putchar ('\n'); - // User has terminated the program, let's save the history and clean up + struct error *e = NULL; char *dir = xstrdup (history_path); - (void) mkdir_with_parents (dirname (dir), NULL); - free (dir); - if (write_history (history_path)) + if (!mkdir_with_parents (dirname (dir), &e) + || !g_ctx.input->vtable->save_history (g_ctx.input, history_path, &e)) + { print_error ("writing the history file `%s' failed: %s", - history_path, strerror (errno)); + history_path, e->message); + error_free (e); + } + free (dir); free (history_path); + iconv_close (g_ctx.term_from_utf8); iconv_close (g_ctx.term_to_utf8); g_ctx.backend->destroy (&g_ctx); free (origin); - free (g_ctx.readline_prompt); config_free (&g_ctx.config); free_terminal (); + g_ctx.input->vtable->destroy (g_ctx.input); ev_loop_destroy (loop); return EXIT_SUCCESS; }