Code

7abfc4ac203a65270f65c0176b812d336ca64d43
[ncmpc.git] / src / wreadln.c
1 /*
2  * (c) 2004 by Kalle Wallin <kaw@linux.se>
3  *
4  * This program is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation; either version 2 of the License, or
7  * (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  * You should have received a copy of the GNU General Public License
14  * along with this program; if not, write to the Free Software
15  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16  *
17  */
19 #include "wreadln.h"
20 #include "charset.h"
21 #include "screen_utils.h"
22 #include "config.h"
24 #include <assert.h>
25 #include <stdlib.h>
26 #include <string.h>
27 #include <glib.h>
29 #ifdef ENABLE_WIDE
30 #include <sys/poll.h>
31 #endif
33 #define KEY_CTRL_A   1
34 #define KEY_CTRL_B   2
35 #define KEY_CTRL_C   3
36 #define KEY_CTRL_D   4
37 #define KEY_CTRL_E   5
38 #define KEY_CTRL_F   6
39 #define KEY_CTRL_G   7
40 #define KEY_CTRL_K   11
41 #define KEY_CTRL_N   14
42 #define KEY_CTRL_P   16
43 #define KEY_CTRL_U   21
44 #define KEY_CTRL_Z   26
45 #define KEY_BCKSPC   8
46 #define TAB          9
48 struct wreadln {
49         /** the ncurses window where this field is displayed */
50         WINDOW *const w;
52         /** the origin coordinates in the window */
53         unsigned x, y;
55         /** the screen width of the input field */
56         unsigned width;
58         /** is the input masked, i.e. characters displayed as '*'? */
59         const gboolean masked;
61         /** the byte position of the cursor */
62         size_t cursor;
64         /** the byte position displayed at the origin (for horizontal
65             scrolling) */
66         size_t start;
68         /** the current value */
69         gchar line[1024];
70 };
72 /** max items stored in the history list */
73 static const guint wrln_max_history_length = 32;
75 void *wrln_completion_callback_data = NULL;
76 wrln_gcmp_pre_cb_t wrln_pre_completion_callback = NULL;
77 wrln_gcmp_post_cb_t wrln_post_completion_callback = NULL;
79 /** converts a byte position to a screen column */
80 static unsigned
81 byte_to_screen(const gchar *data, size_t x)
82 {
83 #ifdef ENABLE_WIDE
84         gchar *dup;
85         char *p;
86         unsigned width;
88         assert(x <= strlen(data));
90         dup = g_strdup(data);
91         dup[x] = 0;
92         p = locale_to_utf8(dup);
93         g_free(dup);
95         width = utf8_width(p);
96         g_free(p);
98         return width;
99 #else
100         (void)data;
102         return (unsigned)x;
103 #endif
106 /** finds the first character which doesn't fit on the screen */
107 static size_t
108 screen_to_bytes(const gchar *data, unsigned width)
110 #ifdef ENABLE_WIDE
111         size_t length = strlen(data);
112         gchar *dup = g_strdup(data);
113         char *p;
114         unsigned p_width;
116         while (true) {
117                 dup[length] = 0;
118                 p = locale_to_utf8(dup);
119                 p_width = utf8_width(p);
120                 g_free(p);
121                 if (p_width <= width)
122                         break;
124                 --length;
125         }
127         g_free(dup);
129         return length;
130 #else
131         (void)data;
133         return (size_t)width;
134 #endif
137 /** returns the screen colum where the cursor is located */
138 static unsigned
139 cursor_column(const struct wreadln *wr)
141         return byte_to_screen(wr->line + wr->start,
142                               wr->cursor - wr->start);
145 /** returns the offset in the string to align it at the right border
146     of the screen */
147 static inline size_t
148 right_align_bytes(const gchar *data, size_t right, unsigned width)
150 #ifdef ENABLE_WIDE
151         gchar *dup;
152         size_t start = 0;
154         assert(right <= strlen(data));
156         dup = g_strdup(data);
157         dup[right] = 0;
159         while (dup[start] != 0) {
160                 char *p = locale_to_utf8(dup + start), *q;
161                 unsigned p_width = utf8_width(p);
162                 gunichar c;
164                 if (p_width < width) {
165                         g_free(p);
166                         break;
167                 }
169                 c = g_utf8_get_char(p);
170                 p[g_unichar_to_utf8(c, NULL)] = 0;
171                 q = utf8_to_locale(p);
172                 g_free(p);
174                 start += strlen(q);
175                 g_free(q);
176         }
178         g_free(dup);
180         return start;
181 #else
182         (void)data;
184         return right >= width ? right + 1 - width : 0;
185 #endif
188 /** returns the size (in bytes) of the next character */
189 static inline size_t
190 next_char_size(const gchar *data)
192 #ifdef ENABLE_WIDE
193         char *p = locale_to_utf8(data), *q;
194         gunichar c;
195         size_t size;
197         c = g_utf8_get_char(p);
198         p[g_unichar_to_utf8(c, NULL)] = 0;
199         q = utf8_to_locale(p);
200         g_free(p);
202         size = strlen(q);
203         g_free(q);
205         return size;
206 #else
207         (void)data;
209         return 1;
210 #endif
213 /** returns the size (in bytes) of the previous character */
214 static inline size_t
215 prev_char_size(const gchar *data, size_t x)
217 #ifdef ENABLE_WIDE
218         char *p = locale_to_utf8(data), *q;
219         gunichar c;
220         size_t size;
222         assert(x > 0);
224         q = p;
225         while (true) {
226                 c = g_utf8_get_char(q);
227                 size = g_unichar_to_utf8(c, NULL);
228                 if (size > x)
229                         size = x;
230                 x -= size;
231                 if (x == 0) {
232                         g_free(p);
233                         return size;
234                 }
236                 q += size;
237         }
238 #else
239         (void)data;
240         (void)x;
242         return 1;
243 #endif
246 /* move the cursor one step to the right */
247 static inline void cursor_move_right(struct wreadln *wr)
249         size_t size;
251         if (wr->line[wr->cursor] == 0)
252                 return;
254         size = next_char_size(wr->line + wr->cursor);
255         wr->cursor += size;
256         if (cursor_column(wr) >= wr->width)
257                 wr->start = right_align_bytes(wr->line, wr->cursor, wr->width);
260 /* move the cursor one step to the left */
261 static inline void cursor_move_left(struct wreadln *wr)
263         size_t size;
265         if (wr->cursor == 0)
266                 return;
268         size = prev_char_size(wr->line, wr->cursor);
269         assert(wr->cursor >= size);
270         wr->cursor -= size;
271         if (wr->cursor < wr->start)
272                 wr->start = wr->cursor;
275 /* move the cursor to the end of the line */
276 static inline void cursor_move_to_eol(struct wreadln *wr)
278         wr->cursor = strlen(wr->line);
279         if (cursor_column(wr) >= wr->width)
280                 wr->start = right_align_bytes(wr->line, wr->cursor, wr->width);
283 /* draw line buffer and update cursor position */
284 static inline void drawline(const struct wreadln *wr)
286         wmove(wr->w, wr->y, wr->x);
287         /* clear input area */
288         whline(wr->w, ' ', wr->width);
289         /* print visible part of the line buffer */
290         if (wr->masked)
291                 whline(wr->w, '*', utf8_width(wr->line + wr->start));
292         else
293                 waddnstr(wr->w, wr->line + wr->start,
294                          screen_to_bytes(wr->line, wr->width));
295         /* move the cursor to the correct position */
296         wmove(wr->w, wr->y, wr->x + cursor_column(wr));
297         /* tell ncurses to redraw the screen */
298         doupdate();
301 #ifdef ENABLE_WIDE
302 static bool
303 multibyte_is_complete(const char *p, size_t length)
305         GError *error = NULL;
306         gchar *q = g_locale_to_utf8(p, length,
307                                     NULL, NULL, &error);
308         if (q != NULL) {
309                 g_free(q);
310                 return true;
311         } else {
312                 g_error_free(error);
313                 return false;
314         }
316 #endif
318 static void
319 wreadln_insert_byte(struct wreadln *wr, gint key)
321         size_t rest = strlen(wr->line + wr->cursor) + 1;
322 #ifdef ENABLE_WIDE
323         char buffer[32] = { key };
324         size_t length = 1;
325         struct pollfd pfd = {
326                 .fd = 0,
327                 .events = POLLIN,
328         };
329         int ret;
331         /* wide version: try to complete the multibyte sequence */
333         while (length < sizeof(buffer)) {
334                 if (multibyte_is_complete(buffer, length))
335                         /* sequence is complete */
336                         break;
338                 /* poll for more bytes on stdin, without timeout */
340                 ret = poll(&pfd, 1, 0);
341                 if (ret <= 0)
342                         /* no more input from keyboard */
343                         break;
345                 buffer[length++] = wgetch(wr->w);
346         }
348         memmove(wr->line + wr->cursor + length,
349                 wr->line + wr->cursor, rest);
350         memcpy(wr->line + wr->cursor, buffer, length);
352 #else
353         const size_t length = 1;
355         memmove(wr->line + wr->cursor + length,
356                 wr->line + wr->cursor, rest);
357         wr->line[wr->cursor] = key;
359 #endif
361         wr->cursor += length;
362         if (cursor_column(wr) >= wr->width)
363                 wr->start = right_align_bytes(wr->line, wr->cursor, wr->width);
366 static void
367 wreadln_delete_char(struct wreadln *wr, size_t x)
369         size_t rest, length;
371         assert(x < strlen(wr->line));
373         length = next_char_size(&wr->line[x]);
374         rest = strlen(&wr->line[x + length]) + 1;
375         memmove(&wr->line[x], &wr->line[x + length], rest);
378 /* libcurses version */
380 static gchar *
381 _wreadln(WINDOW *w,
382          const gchar *prompt,
383          const gchar *initial_value,
384          unsigned x1,
385          GList **history,
386          GCompletion *gcmp,
387          gboolean masked)
389         struct wreadln wr = {
390                 .w = w,
391                 .masked = masked,
392                 .cursor = 0,
393                 .start = 0,
394         };
395         GList *hlist = NULL, *hcurrent = NULL;
396         gint key = 0;
397         size_t i;
399         /* turn off echo */
400         noecho();
401         /* make shure the cursor is visible */
402         curs_set(1);
403         /* print prompt string */
404         if (prompt)
405                 waddstr(w, prompt);
406         /* retrive y and x0 position */
407         getyx(w, wr.y, wr.x);
408         /* check the x1 value */
409         if (x1 <= wr.x || x1 > (unsigned)COLS)
410                 x1 = COLS;
411         wr.width = x1 - wr.x;
412         /* clear input area */
413         mvwhline(w, wr.y, wr.x, ' ', wr.width);
415         if (history) {
416                 /* append the a new line to our history list */
417                 *history = g_list_append(*history, g_malloc0(sizeof(wr.line)));
418                 /* hlist points to the current item in the history list */
419                 hlist = g_list_last(*history);
420                 hcurrent = hlist;
421         }
423         if (initial_value == (char *)-1) {
424                 /* get previous history entry */
425                 if (history && hlist->prev) {
426                         if (hlist == hcurrent)
427                                 /* save the current line */
428                                 g_strlcpy(hlist->data, wr.line, sizeof(wr.line));
430                         /* get previous line */
431                         hlist = hlist->prev;
432                         g_strlcpy(wr.line, hlist->data, sizeof(wr.line));
433                 }
434                 cursor_move_to_eol(&wr);
435                 drawline(&wr);
436         } else if (initial_value) {
437                 /* copy the initial value to the line buffer */
438                 g_strlcpy(wr.line, initial_value, sizeof(wr.line));
439                 cursor_move_to_eol(&wr);
440                 drawline(&wr);
441         }
443         while (key != 13 && key != '\n') {
444                 key = wgetch(w);
446                 /* check if key is a function key */
447                 for (i = 0; i < 63; i++)
448                         if (key == (int)KEY_F(i)) {
449                                 key = KEY_F(1);
450                                 i = 64;
451                         }
453                 switch (key) {
454 #ifdef HAVE_GETMOUSE
455                 case KEY_MOUSE: /* ignore mouse events */
456 #endif
457                 case ERR: /* ingnore errors */
458                         break;
460                 case TAB:
461                         if (gcmp) {
462                                 char *prefix = NULL;
463                                 GList *list;
465                                 if (wrln_pre_completion_callback)
466                                         wrln_pre_completion_callback(gcmp, wr.line,
467                                                                      wrln_completion_callback_data);
468                                 list = g_completion_complete(gcmp, wr.line, &prefix);
469                                 if (prefix) {
470                                         g_strlcpy(wr.line, prefix, sizeof(wr.line));
471                                         cursor_move_to_eol(&wr);
472                                         g_free(prefix);
473                                 } else
474                                         screen_bell();
476                                 if (wrln_post_completion_callback)
477                                         wrln_post_completion_callback(gcmp, wr.line, list,
478                                                                       wrln_completion_callback_data);
479                         }
480                         break;
482                 case KEY_CTRL_G:
483                         screen_bell();
484                         if (history) {
485                                 g_free(hcurrent->data);
486                                 hcurrent->data = NULL;
487                                 *history = g_list_delete_link(*history, hcurrent);
488                         }
489                         return NULL;
491                 case KEY_LEFT:
492                 case KEY_CTRL_B:
493                         cursor_move_left(&wr);
494                         break;
495                 case KEY_RIGHT:
496                 case KEY_CTRL_F:
497                         cursor_move_right(&wr);
498                         break;
499                 case KEY_HOME:
500                 case KEY_CTRL_A:
501                         wr.cursor = 0;
502                         wr.start = 0;
503                         break;
504                 case KEY_END:
505                 case KEY_CTRL_E:
506                         cursor_move_to_eol(&wr);
507                         break;
508                 case KEY_CTRL_K:
509                         wr.line[wr.cursor] = 0;
510                         break;
511                 case KEY_CTRL_U:
512                         wr.cursor = utf8_width(wr.line);
513                         for (i = 0; i < wr.cursor; i++)
514                                 wr.line[i] = '\0';
515                         wr.cursor = 0;
516                         break;
517                 case 127:
518                 case KEY_BCKSPC:        /* handle backspace: copy all */
519                 case KEY_BACKSPACE:     /* chars starting from curpos */
520                         if (wr.cursor > 0) { /* - 1 from buf[n+1] to buf   */
521                                 cursor_move_left(&wr);
522                                 wreadln_delete_char(&wr, wr.cursor);
523                         }
524                         break;
525                 case KEY_DC:            /* handle delete key. As above */
526                 case KEY_CTRL_D:
527                         if (wr.line[wr.cursor] != 0)
528                                 wreadln_delete_char(&wr, wr.cursor);
529                         break;
530                 case KEY_UP:
531                 case KEY_CTRL_P:
532                         /* get previous history entry */
533                         if (history && hlist->prev) {
534                                 if (hlist == hcurrent)
535                                         /* save the current line */
536                                         g_strlcpy(hlist->data, wr.line,
537                                                   sizeof(wr.line));
539                                 /* get previous line */
540                                 hlist = hlist->prev;
541                                 g_strlcpy(wr.line, hlist->data,
542                                           sizeof(wr.line));
543                         }
544                         cursor_move_to_eol(&wr);
545                         break;
546                 case KEY_DOWN:
547                 case KEY_CTRL_N:
548                         /* get next history entry */
549                         if (history && hlist->next) {
550                                 /* get next line */
551                                 hlist = hlist->next;
552                                 g_strlcpy(wr.line, hlist->data,
553                                           sizeof(wr.line));
554                         }
555                         cursor_move_to_eol(&wr);
556                         break;
558                 case '\n':
559                 case 13:
560                 case KEY_IC:
561                 case KEY_PPAGE:
562                 case KEY_NPAGE:
563                 case KEY_F(1):
564                         /* ignore char */
565                         break;
566                 default:
567                         if (key >= 32)
568                                 wreadln_insert_byte(&wr, key);
569                 }
571                 drawline(&wr);
572         }
574         /* update history */
575         if (history) {
576                 if (strlen(wr.line)) {
577                         /* update the current history entry */
578                         size_t size = strlen(wr.line) + 1;
579                         hcurrent->data = g_realloc(hcurrent->data, size);
580                         g_strlcpy(hcurrent->data, wr.line, size);
581                 } else {
582                         /* the line was empty - remove the current history entry */
583                         g_free(hcurrent->data);
584                         hcurrent->data = NULL;
585                         *history = g_list_delete_link(*history, hcurrent);
586                 }
588                 while (g_list_length(*history) > wrln_max_history_length) {
589                         GList *first = g_list_first(*history);
591                         /* remove the oldest history entry  */
592                         g_free(first->data);
593                         first->data = NULL;
594                         *history = g_list_delete_link(*history, first);
595                 }
596         }
598         return g_strdup(wr.line);
601 gchar *
602 wreadln(WINDOW *w,
603         const gchar *prompt,
604         const gchar *initial_value,
605         unsigned x1,
606         GList **history,
607         GCompletion *gcmp)
609         return  _wreadln(w, prompt, initial_value, x1, history, gcmp, FALSE);
612 gchar *
613 wreadln_masked(WINDOW *w,
614                const gchar *prompt,
615                const gchar *initial_value,
616                unsigned x1,
617                GList **history,
618                GCompletion *gcmp)
620         return  _wreadln(w, prompt, initial_value, x1, history, gcmp, TRUE);