X-Git-Url: https://git.tokkee.org/?a=blobdiff_plain;f=src%2Fwreadln.c;h=95267e28d539d28a79b77cf734c23e1a67320fcb;hb=b3e29d017a77d1cd34bfde94bd7182422e6d695c;hp=edf4f702f8d3a624bdad37a10c6c7da98514130c;hpb=4966421e402395e12bf22f75d438504937432e3e;p=ncmpc.git diff --git a/src/wreadln.c b/src/wreadln.c index edf4f70..95267e2 100644 --- a/src/wreadln.c +++ b/src/wreadln.c @@ -1,5 +1,6 @@ -/* - * (c) 2004 by Kalle Wallin +/* ncmpc (Ncurses MPD Client) + * (c) 2004-2017 The Music Player Daemon Project + * Project homepage: http://musicpd.org * * 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 @@ -10,10 +11,10 @@ * 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 * + * 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., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "wreadln.h" @@ -21,10 +22,15 @@ #include "screen_utils.h" #include "config.h" +#include #include #include #include +#if (defined(HAVE_CURSES_ENHANCED) || defined(ENABLE_MULTIBYTE)) && !defined(WIN32) +#include +#endif + #define KEY_CTRL_A 1 #define KEY_CTRL_B 2 #define KEY_CTRL_C 3 @@ -36,121 +42,367 @@ #define KEY_CTRL_N 14 #define KEY_CTRL_P 16 #define KEY_CTRL_U 21 +#define KEY_CTRL_W 23 #define KEY_CTRL_Z 26 #define KEY_BCKSPC 8 #define TAB 9 -#define WRLN_MAX_LINE_SIZE 1024 -#define WRLN_MAX_HISTORY_LENGTH 32 - -guint wrln_max_line_size = WRLN_MAX_LINE_SIZE; -guint wrln_max_history_length = WRLN_MAX_HISTORY_LENGTH; +struct wreadln { + /** the ncurses window where this field is displayed */ + WINDOW *const w; + + /** the origin coordinates in the window */ + unsigned x, y; + + /** the screen width of the input field */ + unsigned width; + + /** is the input masked, i.e. characters displayed as '*'? */ + const gboolean masked; + + /** the byte position of the cursor */ + size_t cursor; + + /** the byte position displayed at the origin (for horizontal + scrolling) */ + size_t start; + + /** the current value */ + gchar line[1024]; +}; + +/** max items stored in the history list */ +static const guint wrln_max_history_length = 32; + +#ifndef NCMPC_MINI void *wrln_completion_callback_data = NULL; wrln_gcmp_pre_cb_t wrln_pre_completion_callback = NULL; wrln_gcmp_post_cb_t wrln_post_completion_callback = NULL; +#endif -/* move the cursor one step to the right */ -static inline void cursor_move_right(gint *cursor, - gint *start, - gint width, - gint x0, - gint x1, - gchar *line) +/** converts a byte position to a screen column */ +static unsigned +byte_to_screen(const gchar *data, size_t x) +{ +#if defined(HAVE_CURSES_ENHANCED) || defined(ENABLE_MULTIBYTE) + assert(x <= strlen(data)); + + char *dup = g_strdup(data); + dup[x] = 0; + char *p = replace_locale_to_utf8(dup); + + unsigned width = utf8_width(p); + g_free(p); + + return width; +#else + (void)data; + + return (unsigned)x; +#endif +} + +/** finds the first character which doesn't fit on the screen */ +static size_t +screen_to_bytes(const gchar *data, unsigned width) { - if (*cursor < (int)strlen(line) && - *cursor < (int)wrln_max_line_size - 1) { - (*cursor)++; - if (*cursor + x0 >= x1 && *start < *cursor - width + 1) - (*start)++; +#if defined(HAVE_CURSES_ENHANCED) || defined(ENABLE_MULTIBYTE) + size_t length = strlen(data); + gchar *dup = g_strdup(data); + + while (true) { + dup[length] = 0; + char *p = locale_to_utf8(dup); + unsigned p_width = utf8_width(p); + g_free(p); + if (p_width <= width) + break; + + --length; } + + g_free(dup); + + return length; +#else + (void)data; + + return (size_t)width; +#endif } -/* move the cursor one step to the left */ -static inline void cursor_move_left(gint *cursor, - gint *start) +/** returns the screen column where the cursor is located */ +static unsigned +cursor_column(const struct wreadln *wr) +{ + return byte_to_screen(wr->line + wr->start, + wr->cursor - wr->start); +} + +/** returns the offset in the string to align it at the right border + of the screen */ +static inline size_t +right_align_bytes(const gchar *data, size_t right, unsigned width) +{ +#if defined(HAVE_CURSES_ENHANCED) || defined(ENABLE_MULTIBYTE) + gchar *dup; + size_t start = 0; + + assert(right <= strlen(data)); + + dup = g_strdup(data); + dup[right] = 0; + + while (dup[start] != 0) { + char *p = locale_to_utf8(dup + start), *q; + unsigned p_width = utf8_width(p); + + if (p_width < width) { + g_free(p); + break; + } + + gunichar c = g_utf8_get_char(p); + p[g_unichar_to_utf8(c, NULL)] = 0; + q = utf8_to_locale(p); + g_free(p); + + start += strlen(q); + g_free(q); + } + + g_free(dup); + + return start; +#else + (void)data; + + return right >= width ? right + 1 - width : 0; +#endif +} + +/** returns the size (in bytes) of the next character */ +static inline size_t +next_char_size(const gchar *data) +{ +#if defined(HAVE_CURSES_ENHANCED) || defined(ENABLE_MULTIBYTE) + char *p = locale_to_utf8(data), *q; + + gunichar c = g_utf8_get_char(p); + p[g_unichar_to_utf8(c, NULL)] = 0; + q = utf8_to_locale(p); + g_free(p); + + size_t size = strlen(q); + g_free(q); + + return size; +#else + (void)data; + + return 1; +#endif +} + +/** returns the size (in bytes) of the previous character */ +static inline size_t +prev_char_size(const gchar *data, size_t x) { - if (*cursor > 0) { - if (*cursor == *start && *start > 0) - (*start)--; - (*cursor)--; +#if defined(HAVE_CURSES_ENHANCED) || defined(ENABLE_MULTIBYTE) + assert(x > 0); + + char *p = locale_to_utf8(data); + + char *q = p; + while (true) { + gunichar c = g_utf8_get_char(q); + size_t size = g_unichar_to_utf8(c, NULL); + if (size > x) + size = x; + x -= size; + if (x == 0) { + g_free(p); + return size; + } + + q += size; } +#else + (void)data; + (void)x; + + return 1; +#endif +} + +/* move the cursor one step to the right */ +static inline void cursor_move_right(struct wreadln *wr) +{ + if (wr->line[wr->cursor] == 0) + return; + + size_t size = next_char_size(wr->line + wr->cursor); + wr->cursor += size; + if (cursor_column(wr) >= wr->width) + wr->start = right_align_bytes(wr->line, wr->cursor, wr->width); +} + +/* move the cursor one step to the left */ +static inline void cursor_move_left(struct wreadln *wr) +{ + if (wr->cursor == 0) + return; + + size_t size = prev_char_size(wr->line, wr->cursor); + assert(wr->cursor >= size); + wr->cursor -= size; + if (wr->cursor < wr->start) + wr->start = wr->cursor; } /* move the cursor to the end of the line */ -static inline void cursor_move_to_eol(gint *cursor, - gint *start, - gint width, - gint x0, - gint x1, - gchar *line) +static inline void cursor_move_to_eol(struct wreadln *wr) { - *cursor = strlen(line); - if (*cursor + x0 >= x1) - *start = *cursor - width + 1; + wr->cursor = strlen(wr->line); + if (cursor_column(wr) >= wr->width) + wr->start = right_align_bytes(wr->line, wr->cursor, wr->width); } /* draw line buffer and update cursor position */ -static inline void drawline(gint cursor, - gint start, - gint width, - gint x0, - gint y, - gboolean masked, - gchar *line, - WINDOW *w) +static inline void drawline(const struct wreadln *wr) { - wmove(w, y, x0); + wmove(wr->w, wr->y, wr->x); /* clear input area */ - whline(w, ' ', width); + whline(wr->w, ' ', wr->width); /* print visible part of the line buffer */ - if(masked == TRUE) - whline(w, '*', utf8_width(line) - start); + if (wr->masked) + whline(wr->w, '*', utf8_width(wr->line + wr->start)); else - waddnstr(w, line+start, width); + waddnstr(wr->w, wr->line + wr->start, + screen_to_bytes(wr->line, wr->width)); /* move the cursor to the correct position */ - wmove(w, y, x0 + cursor-start); + wmove(wr->w, wr->y, wr->x + cursor_column(wr)); /* tell ncurses to redraw the screen */ doupdate(); } +#if (defined(HAVE_CURSES_ENHANCED) || defined(ENABLE_MULTIBYTE)) && !defined(WIN32) +static bool +multibyte_is_complete(const char *p, size_t length) +{ + GError *error = NULL; + gchar *q = g_locale_to_utf8(p, length, + NULL, NULL, &error); + if (q != NULL) { + g_free(q); + return true; + } else { + g_error_free(error); + return false; + } +} +#endif + +static void +wreadln_insert_byte(struct wreadln *wr, gint key) +{ + size_t rest = strlen(wr->line + wr->cursor) + 1; +#if (defined(HAVE_CURSES_ENHANCED) || defined(ENABLE_MULTIBYTE)) && !defined (WIN32) + char buffer[32] = { key }; + size_t length = 1; + struct pollfd pfd = { + .fd = 0, + .events = POLLIN, + }; + + /* wide version: try to complete the multibyte sequence */ + + while (length < sizeof(buffer)) { + if (multibyte_is_complete(buffer, length)) + /* sequence is complete */ + break; + + /* poll for more bytes on stdin, without timeout */ + + if (poll(&pfd, 1, 0) <= 0) + /* no more input from keyboard */ + break; + + buffer[length++] = wgetch(wr->w); + } + + memmove(wr->line + wr->cursor + length, + wr->line + wr->cursor, rest); + memcpy(wr->line + wr->cursor, buffer, length); + +#else + const size_t length = 1; + + memmove(wr->line + wr->cursor + length, + wr->line + wr->cursor, rest); + wr->line[wr->cursor] = key; + +#endif + + wr->cursor += length; + if (cursor_column(wr) >= wr->width) + wr->start = right_align_bytes(wr->line, wr->cursor, wr->width); +} + +static void +wreadln_delete_char(struct wreadln *wr, size_t x) +{ + assert(x < strlen(wr->line)); + + size_t length = next_char_size(&wr->line[x]); + size_t rest = strlen(&wr->line[x + length]) + 1; + memmove(&wr->line[x], &wr->line[x + length], rest); +} + /* libcurses version */ static gchar * _wreadln(WINDOW *w, const gchar *prompt, const gchar *initial_value, - gint x1, + unsigned x1, GList **history, GCompletion *gcmp, gboolean masked) { + struct wreadln wr = { + .w = w, + .masked = masked, + .cursor = 0, + .start = 0, + }; GList *hlist = NULL, *hcurrent = NULL; - gchar *line; - gint x0, y, width; - gint cursor = 0, start = 0; - gint key = 0, i; - /* allocate a line buffer */ - line = g_malloc0(wrln_max_line_size); +#ifdef NCMPC_MINI + (void)gcmp; +#endif + /* turn off echo */ noecho(); - /* make shure the cursor is visible */ + /* make sure the cursor is visible */ curs_set(1); /* print prompt string */ - if (prompt) + if (prompt) { waddstr(w, prompt); - /* retrive y and x0 position */ - getyx(w, y, x0); + waddstr(w, ": "); + } + /* retrieve y and x0 position */ + getyx(w, wr.y, wr.x); /* check the x1 value */ - if (x1 <= x0 || x1 > COLS) + if (x1 <= wr.x || x1 > (unsigned)COLS) x1 = COLS; - width = x1 - x0; + wr.width = x1 - wr.x; /* clear input area */ - mvwhline(w, y, x0, ' ', width); + mvwhline(w, wr.y, wr.x, ' ', wr.width); if (history) { /* append the a new line to our history list */ - *history = g_list_append(*history, g_malloc0(wrln_max_line_size)); + *history = g_list_append(*history, g_malloc0(sizeof(wr.line))); /* hlist points to the current item in the history list */ hlist = g_list_last(*history); hcurrent = hlist; @@ -161,63 +413,67 @@ _wreadln(WINDOW *w, if (history && hlist->prev) { if (hlist == hcurrent) /* save the current line */ - g_strlcpy(hlist->data, line, wrln_max_line_size); + g_strlcpy(hlist->data, wr.line, sizeof(wr.line)); /* get previous line */ hlist = hlist->prev; - g_strlcpy(line, hlist->data, wrln_max_line_size); + g_strlcpy(wr.line, hlist->data, sizeof(wr.line)); } - cursor_move_to_eol(&cursor, &start, width, x0, x1, line); - drawline(cursor, start, width, x0, y, masked, line, w); + cursor_move_to_eol(&wr); + drawline(&wr); } else if (initial_value) { /* copy the initial value to the line buffer */ - g_strlcpy(line, initial_value, wrln_max_line_size); - cursor_move_to_eol(&cursor, &start, width, x0, x1, line); - drawline(cursor, start, width, x0, y, masked, line, w); + g_strlcpy(wr.line, initial_value, sizeof(wr.line)); + cursor_move_to_eol(&wr); + drawline(&wr); } + gint key = 0; while (key != 13 && key != '\n') { key = wgetch(w); /* check if key is a function key */ - for (i = 0; i < 63; i++) - if (key == KEY_F(i)) { + for (size_t i = 0; i < 63; i++) + if (key == (int)KEY_F(i)) { key = KEY_F(1); i = 64; } switch (key) { + size_t i; + #ifdef HAVE_GETMOUSE case KEY_MOUSE: /* ignore mouse events */ #endif - case ERR: /* ingnore errors */ + case ERR: /* ignore errors */ break; case TAB: +#ifndef NCMPC_MINI if (gcmp) { char *prefix = NULL; GList *list; if (wrln_pre_completion_callback) - wrln_pre_completion_callback(gcmp, line, + wrln_pre_completion_callback(gcmp, wr.line, wrln_completion_callback_data); - list = g_completion_complete(gcmp, line, &prefix); + list = g_completion_complete(gcmp, wr.line, &prefix); if (prefix) { - g_strlcpy(line, prefix, wrln_max_line_size); - cursor_move_to_eol(&cursor, &start, width, x0, x1, line); + g_strlcpy(wr.line, prefix, sizeof(wr.line)); + cursor_move_to_eol(&wr); g_free(prefix); } else screen_bell(); if (wrln_post_completion_callback) - wrln_post_completion_callback(gcmp, line, list, + wrln_post_completion_callback(gcmp, wr.line, list, wrln_completion_callback_data); } +#endif break; case KEY_CTRL_G: screen_bell(); - g_free(line); if (history) { g_free(hcurrent->data); hcurrent->data = NULL; @@ -227,45 +483,56 @@ _wreadln(WINDOW *w, case KEY_LEFT: case KEY_CTRL_B: - cursor_move_left(&cursor, &start); + cursor_move_left(&wr); break; case KEY_RIGHT: case KEY_CTRL_F: - cursor_move_right(&cursor, &start, width, x0, x1, line); + cursor_move_right(&wr); break; case KEY_HOME: case KEY_CTRL_A: - cursor = 0; - start = 0; + wr.cursor = 0; + wr.start = 0; break; case KEY_END: case KEY_CTRL_E: - cursor_move_to_eol(&cursor, &start, width, x0, x1, line); + cursor_move_to_eol(&wr); break; case KEY_CTRL_K: - line[cursor] = 0; + wr.line[wr.cursor] = 0; break; case KEY_CTRL_U: - cursor = utf8_width(line); - for (i = 0;i < cursor; i++) - line[i] = '\0'; - cursor = 0; + wr.cursor = utf8_width(wr.line); + for (i = 0; i < wr.cursor; i++) + wr.line[i] = '\0'; + wr.cursor = 0; + break; + case KEY_CTRL_W: + /* Firstly remove trailing spaces. */ + for (i = wr.cursor; i > 0 && wr.line[i-1] == ' '; i--) + { + cursor_move_left(&wr); + wreadln_delete_char(&wr, wr.cursor); + } + /* Then remove word until next space. */ + for (; i > 0 && wr.line[i-1] != ' '; i--) + { + cursor_move_left(&wr); + wreadln_delete_char(&wr, wr.cursor); + } break; case 127: case KEY_BCKSPC: /* handle backspace: copy all */ case KEY_BACKSPACE: /* chars starting from curpos */ - if( cursor > 0 ) {/* - 1 from buf[n+1] to buf */ - for (i = cursor - 1; line[i] != 0; i++) - line[i] = line[i + 1]; - cursor_move_left(&cursor, &start); + if (wr.cursor > 0) { /* - 1 from buf[n+1] to buf */ + cursor_move_left(&wr); + wreadln_delete_char(&wr, wr.cursor); } break; case KEY_DC: /* handle delete key. As above */ case KEY_CTRL_D: - if (cursor <= (gint)utf8_width(line) - 1) { - for (i = cursor; line[i] != 0; i++) - line[i] = line[i + 1]; - } + if (wr.line[wr.cursor] != 0) + wreadln_delete_char(&wr, wr.cursor); break; case KEY_UP: case KEY_CTRL_P: @@ -273,13 +540,15 @@ _wreadln(WINDOW *w, if (history && hlist->prev) { if (hlist == hcurrent) /* save the current line */ - g_strlcpy(hlist->data, line, wrln_max_line_size); + g_strlcpy(hlist->data, wr.line, + sizeof(wr.line)); /* get previous line */ hlist = hlist->prev; - g_strlcpy(line, hlist->data, wrln_max_line_size); + g_strlcpy(wr.line, hlist->data, + sizeof(wr.line)); } - cursor_move_to_eol(&cursor, &start, width, x0, x1, line); + cursor_move_to_eol(&wr); break; case KEY_DOWN: case KEY_CTRL_N: @@ -287,9 +556,10 @@ _wreadln(WINDOW *w, if (history && hlist->next) { /* get next line */ hlist = hlist->next; - g_strlcpy(line, hlist->data, wrln_max_line_size); + g_strlcpy(wr.line, hlist->data, + sizeof(wr.line)); } - cursor_move_to_eol(&cursor, &start, width, x0, x1, line); + cursor_move_to_eol(&wr); break; case '\n': @@ -301,37 +571,20 @@ _wreadln(WINDOW *w, /* ignore char */ break; default: - if (key >= 32) { - if (strlen (line + cursor)) { /* if the cursor is */ - /* not at the last pos */ - gchar *tmp = NULL; - gsize size = strlen(line + cursor) + 1; - - tmp = g_malloc0(size); - g_strlcpy (tmp, line + cursor, size); - line[cursor] = key; - line[cursor + 1] = 0; - g_strlcat (&line[cursor + 1], tmp, size); - g_free(tmp); - cursor_move_right(&cursor, &start, width, x0, x1, line); - } else { - line[cursor + 1] = 0; - line[cursor] = key; - cursor_move_right(&cursor, &start, width, x0, x1, line); - } - } + if (key >= 32) + wreadln_insert_byte(&wr, key); } - drawline(cursor, start, width, x0, y, masked, line, w); + drawline(&wr); } /* update history */ if (history) { - if (strlen(line)) { + if (strlen(wr.line)) { /* update the current history entry */ - size_t size = strlen(line)+1; + size_t size = strlen(wr.line) + 1; hcurrent->data = g_realloc(hcurrent->data, size); - g_strlcpy(hcurrent->data, line, size); + g_strlcpy(hcurrent->data, wr.line, size); } else { /* the line was empty - remove the current history entry */ g_free(hcurrent->data); @@ -339,24 +592,29 @@ _wreadln(WINDOW *w, *history = g_list_delete_link(*history, hcurrent); } - while (g_list_length(*history) > wrln_max_history_length) { + unsigned history_length = g_list_length(*history); + while (history_length > wrln_max_history_length) { GList *first = g_list_first(*history); /* remove the oldest history entry */ g_free(first->data); first->data = NULL; *history = g_list_delete_link(*history, first); + --history_length; } } - return g_realloc(line, strlen(line)+1); + if (wr.line[0] == 0) + return NULL; + + return g_strdup(wr.line); } gchar * wreadln(WINDOW *w, const gchar *prompt, const gchar *initial_value, - gint x1, + unsigned x1, GList **history, GCompletion *gcmp) { @@ -367,7 +625,7 @@ gchar * wreadln_masked(WINDOW *w, const gchar *prompt, const gchar *initial_value, - gint x1, + unsigned x1, GList **history, GCompletion *gcmp) {