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