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