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