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