util/LocaleString: add IsIncompleteCharMB()
[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
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 constexpr std::size_t 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         return StringWidthMB(data, x);
94 #else
95         (void)data;
96
97         return (unsigned)x;
98 #endif
99 }
100
101 /** finds the first character which doesn't fit on the screen */
102 gcc_pure
103 static size_t
104 screen_to_bytes(const char *data, unsigned width)
105 {
106 #if defined(HAVE_CURSES_ENHANCED) || defined(ENABLE_MULTIBYTE)
107         size_t length = strlen(data);
108
109         while (true) {
110                 unsigned p_width = StringWidthMB(data, length);
111                 if (p_width <= width)
112                         return length;
113
114                 --length;
115         }
116 #else
117         (void)data;
118
119         return (size_t)width;
120 #endif
121 }
122
123 /** returns the screen column where the cursor is located */
124 gcc_pure
125 static unsigned
126 cursor_column(const struct wreadln *wr)
127 {
128         return byte_to_screen(wr->value.data() + wr->start,
129                               wr->cursor - wr->start);
130 }
131
132 /** returns the offset in the string to align it at the right border
133     of the screen */
134 gcc_pure
135 static inline size_t
136 right_align_bytes(const char *data, size_t right, unsigned width)
137 {
138 #if defined(HAVE_CURSES_ENHANCED) || defined(ENABLE_MULTIBYTE)
139         size_t start = 0;
140
141         assert(right <= strlen(data));
142
143         while (start < right) {
144                 if (StringWidthMB(data + start, right - start) < width)
145                         break;
146
147                 start += CharSizeMB(data + start, right - start);
148         }
149
150         return start;
151 #else
152         (void)data;
153
154         return right >= width ? right + 1 - width : 0;
155 #endif
156 }
157
158 /* move the cursor one step to the right */
159 static inline void cursor_move_right(struct wreadln *wr)
160 {
161         if (wr->cursor == wr->value.length())
162                 return;
163
164         size_t size = CharSizeMB(wr->value.data() + wr->cursor,
165                                  wr->value.length() - wr->cursor);
166         wr->cursor += size;
167         if (cursor_column(wr) >= wr->width)
168                 wr->start = right_align_bytes(wr->value.c_str(),
169                                               wr->cursor, wr->width);
170 }
171
172 /* move the cursor one step to the left */
173 static inline void cursor_move_left(struct wreadln *wr)
174 {
175         const char *v = wr->value.c_str();
176         const char *new_cursor = PrevCharMB(v, v + wr->cursor);
177         wr->cursor = new_cursor - v;
178         if (wr->cursor < wr->start)
179                 wr->start = wr->cursor;
180 }
181
182 /* move the cursor to the end of the line */
183 static inline void cursor_move_to_eol(struct wreadln *wr)
184 {
185         wr->cursor = wr->value.length();
186         if (cursor_column(wr) >= wr->width)
187                 wr->start = right_align_bytes(wr->value.c_str(),
188                                               wr->cursor, wr->width);
189 }
190
191 /* draw line buffer and update cursor position */
192 static inline void drawline(const struct wreadln *wr)
193 {
194         wmove(wr->w, wr->point.y, wr->point.x);
195         /* clear input area */
196         whline(wr->w, ' ', wr->width);
197         /* print visible part of the line buffer */
198         if (wr->masked)
199                 whline(wr->w, '*', utf8_width(wr->value.c_str() + wr->start));
200         else
201                 waddnstr(wr->w, wr->value.c_str() + wr->start,
202                          screen_to_bytes(wr->value.c_str(), wr->width));
203         /* move the cursor to the correct position */
204         wmove(wr->w, wr->point.y, wr->point.x + cursor_column(wr));
205         /* tell ncurses to redraw the screen */
206         doupdate();
207 }
208
209 static void
210 wreadln_insert_byte(struct wreadln *wr, int key)
211 {
212         size_t length = 1;
213 #if (defined(HAVE_CURSES_ENHANCED) || defined(ENABLE_MULTIBYTE)) && !defined(_WIN32)
214         char buffer[32] = { (char)key };
215         struct pollfd pfd = {
216                 .fd = 0,
217                 .events = POLLIN,
218                 .revents = 0,
219         };
220
221         /* wide version: try to complete the multibyte sequence */
222
223         while (length < sizeof(buffer)) {
224                 if (!IsIncompleteCharMB(buffer, length))
225                         /* sequence is complete */
226                         break;
227
228                 /* poll for more bytes on stdin, without timeout */
229
230                 if (poll(&pfd, 1, 0) <= 0)
231                         /* no more input from keyboard */
232                         break;
233
234                 buffer[length++] = wgetch(wr->w);
235         }
236
237         wr->value.insert(wr->cursor, buffer, length);
238
239 #else
240         wr->value.insert(wr->cursor, key);
241 #endif
242
243         wr->cursor += length;
244         if (cursor_column(wr) >= wr->width)
245                 wr->start = right_align_bytes(wr->value.c_str(),
246                                               wr->cursor, wr->width);
247 }
248
249 static void
250 wreadln_delete_char(struct wreadln *wr, size_t x)
251 {
252         assert(x < wr->value.length());
253
254         size_t length = CharSizeMB(wr->value.data() + x,
255                                    wr->value.length() - x);
256         wr->value.erase(x, length);
257 }
258
259 /* libcurses version */
260
261 static std::string
262 _wreadln(WINDOW *w,
263          const char *prompt,
264          const char *initial_value,
265          unsigned x1,
266          History *history,
267          Completion *completion,
268          bool masked)
269 {
270         struct wreadln wr(w, masked);
271         History::iterator hlist, hcurrent;
272
273 #ifdef NCMPC_MINI
274         (void)completion;
275 #endif
276
277         /* turn off echo */
278         noecho();
279         /* make sure the cursor is visible */
280         curs_set(1);
281         /* print prompt string */
282         if (prompt) {
283                 waddstr(w, prompt);
284                 waddstr(w, ": ");
285         }
286         /* retrieve y and x0 position */
287         getyx(w, wr.point.y, wr.point.x);
288         /* check the x1 value */
289         if (x1 <= (unsigned)wr.point.x || x1 > (unsigned)COLS)
290                 x1 = COLS;
291         wr.width = x1 - wr.point.x;
292         /* clear input area */
293         mvwhline(w, wr.point.y, wr.point.x, ' ', wr.width);
294
295         if (history) {
296                 /* append the a new line to our history list */
297                 history->emplace_back();
298                 /* hlist points to the current item in the history list */
299                 hcurrent = hlist = std::prev(history->end());
300         }
301
302         if (initial_value == (char *)-1) {
303                 /* get previous history entry */
304                 if (history && hlist != history->begin()) {
305                         /* get previous line */
306                         --hlist;
307                         wr.value = *hlist;
308                 }
309                 cursor_move_to_eol(&wr);
310                 drawline(&wr);
311         } else if (initial_value) {
312                 /* copy the initial value to the line buffer */
313                 wr.value = initial_value;
314                 cursor_move_to_eol(&wr);
315                 drawline(&wr);
316         }
317
318         int key = 0;
319         while (key != 13 && key != '\n') {
320                 key = wgetch(w);
321
322                 /* check if key is a function key */
323                 for (size_t i = 0; i < 63; i++)
324                         if (key == (int)KEY_F(i)) {
325                                 key = KEY_F(1);
326                                 i = 64;
327                         }
328
329                 switch (key) {
330 #ifdef HAVE_GETMOUSE
331                 case KEY_MOUSE: /* ignore mouse events */
332 #endif
333                 case ERR: /* ignore errors */
334                         break;
335
336                 case TAB:
337 #ifndef NCMPC_MINI
338                         if (completion != nullptr) {
339                                 completion->Pre(wr.value.c_str());
340                                 auto r = completion->Complete(wr.value.c_str());
341                                 if (!r.new_prefix.empty()) {
342                                         wr.value = std::move(r.new_prefix);
343                                         cursor_move_to_eol(&wr);
344                                 } else
345                                         screen_bell();
346
347                                 completion->Post(wr.value.c_str(), r.range);
348                         }
349 #endif
350                         break;
351
352                 case KEY_CTRL_G:
353                         screen_bell();
354                         if (history) {
355                                 history->pop_back();
356                         }
357                         return {};
358
359                 case KEY_LEFT:
360                 case KEY_CTRL_B:
361                         cursor_move_left(&wr);
362                         break;
363                 case KEY_RIGHT:
364                 case KEY_CTRL_F:
365                         cursor_move_right(&wr);
366                         break;
367                 case KEY_HOME:
368                 case KEY_CTRL_A:
369                         wr.cursor = 0;
370                         wr.start = 0;
371                         break;
372                 case KEY_END:
373                 case KEY_CTRL_E:
374                         cursor_move_to_eol(&wr);
375                         break;
376                 case KEY_CTRL_K:
377                         wr.value.erase(wr.cursor);
378                         break;
379                 case KEY_CTRL_U:
380                         wr.value.erase(0, wr.cursor);
381                         wr.cursor = 0;
382                         break;
383                 case KEY_CTRL_W:
384                         /* Firstly remove trailing spaces. */
385                         for (; wr.cursor > 0 && wr.value[wr.cursor - 1] == ' ';)
386                         {
387                                 cursor_move_left(&wr);
388                                 wreadln_delete_char(&wr, wr.cursor);
389                         }
390                         /* Then remove word until next space. */
391                         for (; wr.cursor > 0 && wr.value[wr.cursor - 1] != ' ';)
392                         {
393                                 cursor_move_left(&wr);
394                                 wreadln_delete_char(&wr, wr.cursor);
395                         }
396                         break;
397                 case 127:
398                 case KEY_BCKSPC:        /* handle backspace: copy all */
399                 case KEY_BACKSPACE:     /* chars starting from curpos */
400                         if (wr.cursor > 0) { /* - 1 from buf[n+1] to buf   */
401                                 cursor_move_left(&wr);
402                                 wreadln_delete_char(&wr, wr.cursor);
403                         }
404                         break;
405                 case KEY_DC:            /* handle delete key. As above */
406                 case KEY_CTRL_D:
407                         if (wr.cursor < wr.value.length())
408                                 wreadln_delete_char(&wr, wr.cursor);
409                         break;
410                 case KEY_UP:
411                 case KEY_CTRL_P:
412                         /* get previous history entry */
413                         if (history && hlist != history->begin()) {
414                                 if (hlist == hcurrent)
415                                         /* save the current line */
416                                         *hlist = wr.value;
417
418                                 /* get previous line */
419                                 --hlist;
420                                 wr.value = *hlist;
421                         }
422                         cursor_move_to_eol(&wr);
423                         break;
424                 case KEY_DOWN:
425                 case KEY_CTRL_N:
426                         /* get next history entry */
427                         if (history && std::next(hlist) != history->end()) {
428                                 /* get next line */
429                                 ++hlist;
430                                 wr.value = *hlist;
431                         }
432                         cursor_move_to_eol(&wr);
433                         break;
434
435                 case '\n':
436                 case 13:
437                 case KEY_IC:
438                 case KEY_PPAGE:
439                 case KEY_NPAGE:
440                 case KEY_F(1):
441                         /* ignore char */
442                         break;
443                 default:
444                         if (key >= 32)
445                                 wreadln_insert_byte(&wr, key);
446                 }
447
448                 drawline(&wr);
449         }
450
451         /* update history */
452         if (history) {
453                 if (!wr.value.empty()) {
454                         /* update the current history entry */
455                         *hcurrent = wr.value;
456                 } else {
457                         /* the line was empty - remove the current history entry */
458                         history->erase(hcurrent);
459                 }
460
461                 auto history_length = history->size();
462                 while (history_length > wrln_max_history_length) {
463                         history->pop_front();
464                         --history_length;
465                 }
466         }
467
468         return std::move(wr.value);
469 }
470
471 std::string
472 wreadln(WINDOW *w,
473         const char *prompt,
474         const char *initial_value,
475         unsigned x1,
476         History *history,
477         Completion *completion)
478 {
479         return  _wreadln(w, prompt, initial_value, x1,
480                          history, completion, false);
481 }
482
483 std::string
484 wreadln_masked(WINDOW *w,
485                const char *prompt,
486                const char *initial_value,
487                unsigned x1)
488 {
489         return  _wreadln(w, prompt, initial_value, x1, nullptr, nullptr, true);
490 }