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