diff --git a/CMakeLists.txt b/CMakeLists.txt index a6ee3e9..0840b00 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,10 @@ project (uirc3 C) cmake_minimum_required (VERSION 2.8.5) +# Options +option (WANT_READLINE "Use GNU Readline for the UI (better)" ON) +option (WANT_EDITLINE "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 @@ -38,7 +42,25 @@ else (CURSES_FOUND) message (SEND_ERROR "Curses not found") endif (ncursesw_FOUND) +if ((WANT_READLINE AND WANT_EDITLINE) OR (NOT WANT_READLINE AND NOT WANT_EDITLINE)) + message (SEND_ERROR "You have to choose either GNU Readline or libedit") +elseif (WANT_READLINE) + list (APPEND project_libraries readline) +elseif (WANT_EDITLINE) + pkg_check_modules (libedit REQUIRED libedit) + list (APPEND project_libraries ${libedit_LIBRARIES}) + include_directories (${libedit_INCLUDE_DIRS}) +endif ((WANT_READLINE AND WANT_EDITLINE) OR (NOT WANT_READLINE AND NOT WANT_EDITLINE)) + # Generate a configuration file +if (WANT_READLINE) + set (HAVE_READLINE 1) +endif (WANT_READLINE) + +if (WANT_EDITLINE) + set (HAVE_EDITLINE 1) +endif (WANT_EDITLINE) + include (GNUInstallDirs) set (plugin_dir ${CMAKE_INSTALL_LIBDIR}/${PROJECT_NAME}) configure_file (${PROJECT_SOURCE_DIR}/config.h.in ${PROJECT_BINARY_DIR}/config.h) @@ -62,7 +84,7 @@ target_link_libraries (zyklonb ${project_libraries}) add_executable (degesch degesch.c kike-replies.c ${common_sources} ${common_headers}) -target_link_libraries (degesch ${project_libraries} readline) +target_link_libraries (degesch ${project_libraries}) add_executable (kike kike.c kike-replies.c ${common_sources} ${common_headers}) target_link_libraries (kike ${project_libraries}) diff --git a/README b/README index 46ff41a..6c8bbfb 100644 --- a/README +++ b/README @@ -50,14 +50,15 @@ Notable features: Building -------- Build dependencies: CMake, pkg-config, help2man, awk, sh, liberty (included) -Runtime dependencies: openssl, curses (degesch), readline (degesch) +Runtime dependencies: openssl, curses (degesch), readline or libedit (degesch) $ git clone https://github.com/pjanouch/uirc3.git $ git submodule init $ git submodule update $ mkdir build $ cd build - $ cmake .. -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Debug + $ cmake .. -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Debug \ + -DWANT_READLINE=ON -DWANT_LIBEDIT=OFF $ make To install the application, you can do either the usual: diff --git a/config.h.in b/config.h.in index 265735d..63eca68 100644 --- a/config.h.in +++ b/config.h.in @@ -4,4 +4,7 @@ #define PROGRAM_VERSION "${project_VERSION}" #define PLUGIN_DIR "${CMAKE_INSTALL_PREFIX}/${plugin_dir}" +#cmakedefine HAVE_READLINE +#cmakedefine HAVE_EDITLINE + #endif // ! CONFIG_H diff --git a/degesch.c b/degesch.c index 8ac776f..d5716a5 100644 --- a/degesch.c +++ b/degesch.c @@ -69,32 +69,55 @@ enum #undef lines #undef columns +#ifdef HAVE_READLINE #include #include +#endif // HAVE_READLINE + +#ifdef HAVE_EDITLINE +#include +#endif // HAVE_EDITLINE // --- User interface ---------------------------------------------------------- -// Currently provided by GNU Readline. A libedit backend is also possible. +// I'm not sure which one of these backends is worse: whether it's GNU Readline +// or BSD Editline. They both have their own annoying problems. struct input_buffer { +#ifdef HAVE_READLINE HISTORY_STATE *history; ///< Saved history state char *saved_line; ///< Saved line content - int saved_point; ///< Saved cursor position int saved_mark; ///< Saved mark +#elif defined HAVE_EDITLINE + HistoryW *history; ///< The history object + wchar_t *saved_line; ///< Saved line content + int saved_len; ///< Length of the saved line +#endif // HAVE_EDITLINE + int saved_point; ///< Saved cursor position }; static struct input_buffer * input_buffer_new (void) { struct input_buffer *self = xcalloc (1, sizeof *self); +#ifdef HAVE_EDITLINE + self->history = history_winit (); + + HistEventW ev; + history_w (self->history, &ev, H_SETSIZE, HISTORY_LIMIT); +#endif // HAVE_EDITLINE return self; } static void input_buffer_destroy (struct input_buffer *self) { +#ifdef HAVE_READLINE // Can't really free "history" from here +#elif defined HAVE_EDITLINE + history_wend (self->history); +#endif // HAVE_EDITLINE free (self->saved_line); free (self); } @@ -103,9 +126,15 @@ struct input { bool active; ///< Are we a thing? +#if defined HAVE_READLINE char *saved_line; ///< Saved line content int saved_point; ///< Saved cursor position int saved_mark; ///< Saved mark +#elif defined HAVE_EDITLINE + EditLine *editline; ///< The EditLine object + char *(*saved_prompt) (EditLine *); ///< Saved prompt function + char saved_char; ///< Saved char for the prompt +#endif // HAVE_EDITLINE char *prompt; ///< The prompt we use int prompt_shown; ///< Whether the prompt is shown now @@ -122,12 +151,19 @@ input_init (struct input *self) static void input_free (struct input *self) { +#ifdef HAVE_READLINE free (self->saved_line); +#endif // HAVE_READLINE free (self->prompt); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +#ifdef HAVE_READLINE + +#define INPUT_START_IGNORE RL_PROMPT_START_IGNORE +#define INPUT_END_IGNORE RL_PROMPT_END_IGNORE + #define input_ding(self) rl_ding () static void @@ -181,8 +217,14 @@ static int app_readline_init (void); static void on_readline_input (char *line); static void -input_start (struct input *self) +input_start (struct input *self, const char *program_name) { + (void) program_name; + + using_history (); + // This can cause memory leaks, or maybe even a segfault. Funny, eh? + stifle_history (HISTORY_LIMIT); + rl_startup_hook = app_readline_init; rl_catch_sigwinch = false; rl_callback_handler_install (self->prompt, on_readline_input); @@ -203,53 +245,6 @@ input_stop (struct input *self) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -static void -input_save (struct input *self) -{ - hard_assert (!self->saved_line); - - self->saved_point = rl_point; - self->saved_mark = rl_mark; - self->saved_line = rl_copy_text (0, rl_end); -} - -static void -input_restore (struct input *self) -{ - hard_assert (self->saved_line); - - rl_set_prompt (self->prompt); - rl_replace_line (self->saved_line, 0); - rl_point = self->saved_point; - rl_mark = self->saved_mark; - free (self->saved_line); - self->saved_line = NULL; -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -static void -input_hide (struct input *self) -{ - if (!self->active || self->prompt_shown-- < 1) - return; - - input_save (self); - input_erase (self); -} - -static void -input_show (struct input *self) -{ - if (!self->active || ++self->prompt_shown < 1) - return; - - input_restore (self); - rl_redisplay (); -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // The following part shows you why it's not a good idea to use // GNU Readline for this kind of software. Or for anything else, really. @@ -337,7 +332,7 @@ input_destroy_buffer (struct input *self, struct input_buffer *buffer) #if RL_READLINE_VERSION >= 0x0603 if (buffer->history) { - // See buffer_activate() for why we need to do this BS + // See input_switch_buffer() for why we need to do this BS rl_free_undo_list (); // This is probably the only way we can free the history fully @@ -355,6 +350,275 @@ input_destroy_buffer (struct input *self, struct input_buffer *buffer) input_buffer_destroy (buffer); } +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static void +input_save (struct input *self) +{ + hard_assert (!self->saved_line); + + self->saved_point = rl_point; + self->saved_mark = rl_mark; + self->saved_line = rl_copy_text (0, rl_end); +} + +static void +input_restore (struct input *self) +{ + hard_assert (self->saved_line); + + rl_set_prompt (self->prompt); + rl_replace_line (self->saved_line, 0); + rl_point = self->saved_point; + rl_mark = self->saved_mark; + free (self->saved_line); + self->saved_line = NULL; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static void +input_hide (struct input *self) +{ + if (!self->active || self->prompt_shown-- < 1) + return; + + input_save (self); + input_erase (self); +} + +static void +input_show (struct input *self) +{ + if (!self->active || ++self->prompt_shown < 1) + return; + + input_restore (self); + rl_redisplay (); +} + +#endif // HAVE_READLINE + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +#ifdef HAVE_EDITLINE + +#define INPUT_START_IGNORE '\x01' +#define INPUT_END_IGNORE '\x01' + +static void app_editline_init (struct input *self); +static void on_editline_input (struct input *self, char *line); + +static void +input_ding (struct input *self) +{ + (void) self; + + // XXX: this isn't probably very portable + putc ('\a', stdout); +} + +static void +input_on_terminal_resized (struct input *self) +{ + el_resize (self->editline); +} + +static void +input_redisplay (struct input *self) +{ + // See rl_redisplay() + // The character is VREPRINT (usually C-r) + // TODO: read it from terminal info + // XXX: could we potentially break UTF-8 with this? + char x[] = { ('R' - 'A' + 1), 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 void +input_set_prompt (struct input *self, char *prompt) +{ + free (self->prompt); + self->prompt = prompt; + + if (self->prompt_shown) + input_redisplay (self); +} + +static char * +input_make_prompt (EditLine *editline) +{ + struct input *self; + el_get (editline, EL_CLIENTDATA, &self); + return self->prompt; +} + +static char * +input_make_empty_prompt (EditLine *editline) +{ + (void) editline; + return ""; +} + +static void +input_erase (struct input *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); + + // XXX: this doesn't seem to save the escape character + el_get (self->editline, EL_PROMPT, &self->saved_prompt, &self->saved_char); + el_set (self->editline, EL_PROMPT, input_make_empty_prompt); + input_redisplay (self); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static void +input_start (struct input *self, const char *program_name) +{ + self->editline = el_init (program_name, stdin, stdout, stderr); + el_set (self->editline, EL_CLIENTDATA, self); + el_set (self->editline, EL_PROMPT_ESC, + input_make_prompt, INPUT_START_IGNORE); + el_set (self->editline, EL_SIGNAL, false); + el_set (self->editline, EL_UNBUFFERED, true); + el_set (self->editline, EL_EDITOR, "emacs"); + + app_editline_init (self); + self->prompt_shown = 1; + self->active = true; +} + +static void +input_stop (struct input *self) +{ + if (self->prompt_shown > 0) + input_erase (self); + + el_end (self->editline); + self->editline = NULL; + self->active = false; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static void +input_save_buffer (struct input *self, struct input_buffer *buffer) +{ + 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); + + buffer->saved_line = line; + buffer->saved_point = point; + buffer->saved_len = len; +} + +static void +input_restore_buffer (struct input *self, struct input_buffer *buffer) +{ + if (buffer->saved_line) + { + el_winsertstr (self->editline, buffer->saved_line); + el_cursor (self->editline, + -(buffer->saved_len - buffer->saved_point)); + free (buffer->saved_line); + buffer->saved_line = NULL; + } +} + +static void +input_switch_buffer (struct input *self, struct input_buffer *buffer) +{ + if (self->current) + input_save_buffer (self, self->current); + + input_restore_buffer (self, buffer); + el_wset (self->editline, EL_HIST, history, buffer->history); + self->current = buffer; +} + +static void +input_destroy_buffer (struct input *self, struct input_buffer *buffer) +{ + (void) self; + input_buffer_destroy (buffer); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static void +input_save (struct input *self) +{ + input_save_buffer (self, self->current); +} + +static void +input_restore (struct input *self) +{ + input_restore_buffer (self, self->current); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static void +input_hide (struct input *self) +{ + if (!self->active || self->prompt_shown-- < 1) + return; + + input_save (self); + input_erase (self); +} + +static void +input_show (struct input *self) +{ + if (!self->active || ++self->prompt_shown < 1) + return; + + input_restore (self); + // Would have used "saved_char" but it doesn't seem to work. + // And it doesn't even when it does anyway (it seems to just strip it). + el_set (self->editline, + EL_PROMPT_ESC, input_make_prompt, INPUT_START_IGNORE); + input_redisplay (self); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static void +input_on_readable (struct input *self) +{ + // We bind the return key to process it how we need to + + // 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); + if (!buf || count-- <= 0) + return; + + // The character is VEOF (usually C-d) + // TODO: read it from terminal info + if (count == 0 && buf[0] == ('D' - 'A' + 1)) + input_ding (self); +} + +#endif // HAVE_EDITLINE + // --- Application data -------------------------------------------------------- // All text stored in our data structures is encoded in UTF-8. @@ -2560,18 +2824,18 @@ refresh_prompt (struct app_context *ctx) make_prompt (ctx, &prompt); str_append_c (&prompt, ' '); - if (!have_attributes) - input_set_prompt (&ctx->input, xstrdup (prompt.str)); - else + if (have_attributes) { // XXX: to be completely correct, we should use tputs, but we cannot input_set_prompt (&ctx->input, xstrdup_printf ("%c%s%c%s%c%s%c", - RL_PROMPT_START_IGNORE, ctx->attrs[ATTR_PROMPT], - RL_PROMPT_END_IGNORE, + INPUT_START_IGNORE, ctx->attrs[ATTR_PROMPT], + INPUT_END_IGNORE, prompt.str, - RL_PROMPT_START_IGNORE, ctx->attrs[ATTR_RESET], - RL_PROMPT_END_IGNORE)); + INPUT_START_IGNORE, ctx->attrs[ATTR_RESET], + INPUT_END_IGNORE)); } + else + input_set_prompt (&ctx->input, xstrdup (prompt.str)); str_free (&prompt); } @@ -4738,6 +5002,8 @@ irc_connect (struct server *s, bool *should_retry, struct error **e) // --- User interface actions -------------------------------------------------- +#ifdef HAVE_READLINE + static int on_readline_goto_buffer (int count, int key) { @@ -4755,7 +5021,7 @@ on_readline_goto_buffer (int count, int key) if (ctx->last_buffer && buffer_get_index (ctx, ctx->current_buffer) == n) // Fast switching between two buffers buffer_activate (ctx, ctx->last_buffer); - else if (!buffer_goto (ctx, n == 0 ? 10 : n)) + else if (!buffer_goto (ctx, n)) input_ding (self); return 0; } @@ -4766,8 +5032,7 @@ on_readline_previous_buffer (int count, int key) (void) key; struct app_context *ctx = g_ctx; - if (ctx->current_buffer) - buffer_activate (ctx, buffer_previous (ctx, count)); + buffer_activate (ctx, buffer_previous (ctx, count)); return 0; } @@ -4777,8 +5042,7 @@ on_readline_next_buffer (int count, int key) (void) key; struct app_context *ctx = g_ctx; - if (ctx->current_buffer) - buffer_activate (ctx, buffer_next (ctx, count)); + buffer_activate (ctx, buffer_next (ctx, count)); return 0; } @@ -4862,6 +5126,125 @@ app_readline_init (void) return 0; } +#endif // HAVE_READLINE + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +#ifdef HAVE_EDITLINE + +static unsigned char +on_editline_goto_buffer (EditLine *editline, int key) +{ + (void) editline; + + int n = key - '0'; + if (n < 0 || n > 9) + return CC_ERROR; + + // There's no buffer zero + if (n == 0) + n = 10; + + struct app_context *ctx = g_ctx; + if (ctx->last_buffer && buffer_get_index (ctx, ctx->current_buffer) == n) + // Fast switching between two buffers + buffer_activate (ctx, ctx->last_buffer); + else if (!buffer_goto (ctx, n)) + return CC_ERROR; + return CC_NORM; +} + +static unsigned char +on_editline_previous_buffer (EditLine *editline, int key) +{ + (void) editline; + (void) key; + + struct app_context *ctx = g_ctx; + buffer_activate (ctx, buffer_previous (ctx, 1)); + return CC_NORM; +} + +static unsigned char +on_editline_next_buffer (EditLine *editline, int key) +{ + (void) editline; + (void) key; + + struct app_context *ctx = g_ctx; + buffer_activate (ctx, buffer_next (ctx, 1)); + return CC_NORM; +} + +static unsigned char +on_editline_return (EditLine *editline, int key) +{ + (void) key; + struct input *self = &g_ctx->input; + + 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); + + // XXX: Editline seems to remember its position in history, + // so it's not going to work as you'd expect it to + if (*line) + { + HistEventW ev; + history_w (self->current->history, &ev, H_ENTER, line); + print_debug ("history: %d %ls", ev.num, ev.str); + } + + // Convert it to multibyte to reflect the Readline interface + size_t needed = wcstombs (NULL, line, 0) + 1; + char converted[needed]; + if (!needed || wcstombs (converted, line, needed) == (size_t) -1) + print_error ("encoding conversion failed"); + else + process_input (g_ctx, converted); + + free (line); + + el_cursor (editline, len - point); + el_wdeletestr (editline, len); + return CC_REFRESH; +} + +static void +app_editline_init (struct input *self) +{ + el_set (self->editline, EL_ADDFN, "goto-buffer", + "Go to buffer", on_editline_goto_buffer); + el_set (self->editline, EL_ADDFN, "previous-buffer", + "Previous buffer", on_editline_previous_buffer); + el_set (self->editline, EL_ADDFN, "next-buffer", + "Next buffer", on_editline_next_buffer); + + // Redefine M-0 through M-9 to switch buffers + for (size_t i = 0; i < 10; i++) + { + char keyseq[] = { 'M', '-', '0' + i, 0 }; + el_set (self->editline, EL_BIND, keyseq, "goto-buffer", NULL); + } + + el_set (self->editline, EL_BIND, "M-p", "ed-prev-history", NULL); + el_set (self->editline, EL_BIND, "M-n", "ed-next-history", NULL); + el_set (self->editline, EL_BIND, "^P", "previous-buffer", NULL); + el_set (self->editline, EL_BIND, "^N", "next-buffer", NULL); + + // Source the user's defaults file + el_source (self->editline, NULL); + + el_set (self->editline, EL_ADDFN, "send-line", + "Send line", on_editline_return); + el_set (self->editline, EL_BIND, "\n", "send-line", NULL); +} + +#endif // HAVE_EDITLINE + // --- I/O event handlers ------------------------------------------------------ static void @@ -5095,10 +5478,6 @@ main (int argc, char *argv[]) SSL_load_error_strings (); atexit (ERR_free_strings); - using_history (); - // This can cause memory leaks, or maybe even a segfault. Funny, eh? - stifle_history (HISTORY_LIMIT); - setup_signal_handlers (); register_config_modules (&ctx); load_configuration (&ctx); @@ -5109,7 +5488,7 @@ main (int argc, char *argv[]) buffer_activate (&ctx, ctx.server.buffer); refresh_prompt (&ctx); - input_start (&ctx.input); + input_start (&ctx.input, argv[0]); // Connect to the server ASAP poller_timer_set (&ctx.server.reconnect_tmr, 0);