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