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