1 /* ncmpc (Ncurses MPD Client)
2 * (c) 2004-2010 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 */
20 #include "wreadln.h"
21 #include "charset.h"
22 #include "screen_utils.h"
23 #include "config.h"
25 #include <assert.h>
26 #include <stdlib.h>
27 #include <string.h>
28 #include <glib.h>
30 #if (defined(HAVE_CURSES_ENHANCED) || defined(ENABLE_MULTIBYTE)) && !defined(WIN32)
31 #include <sys/poll.h>
32 #endif
34 #define KEY_CTRL_A 1
35 #define KEY_CTRL_B 2
36 #define KEY_CTRL_C 3
37 #define KEY_CTRL_D 4
38 #define KEY_CTRL_E 5
39 #define KEY_CTRL_F 6
40 #define KEY_CTRL_G 7
41 #define KEY_CTRL_K 11
42 #define KEY_CTRL_N 14
43 #define KEY_CTRL_P 16
44 #define KEY_CTRL_U 21
45 #define KEY_CTRL_W 23
46 #define KEY_CTRL_Z 26
47 #define KEY_BCKSPC 8
48 #define TAB 9
50 struct wreadln {
51 /** the ncurses window where this field is displayed */
52 WINDOW *const w;
54 /** the origin coordinates in the window */
55 unsigned x, y;
57 /** the screen width of the input field */
58 unsigned width;
60 /** is the input masked, i.e. characters displayed as '*'? */
61 const gboolean masked;
63 /** the byte position of the cursor */
64 size_t cursor;
66 /** the byte position displayed at the origin (for horizontal
67 scrolling) */
68 size_t start;
70 /** the current value */
71 gchar line[1024];
72 };
74 /** max items stored in the history list */
75 static const guint wrln_max_history_length = 32;
77 #ifndef NCMPC_MINI
78 void *wrln_completion_callback_data = NULL;
79 wrln_gcmp_pre_cb_t wrln_pre_completion_callback = NULL;
80 wrln_gcmp_post_cb_t wrln_post_completion_callback = NULL;
81 #endif
83 /** converts a byte position to a screen column */
84 static unsigned
85 byte_to_screen(const gchar *data, size_t x)
86 {
87 #if defined(HAVE_CURSES_ENHANCED) || defined(ENABLE_MULTIBYTE)
88 gchar *dup;
89 char *p;
90 unsigned width;
92 assert(x <= strlen(data));
94 dup = g_strdup(data);
95 dup[x] = 0;
96 p = replace_locale_to_utf8(dup);
98 width = utf8_width(p);
99 g_free(p);
101 return width;
102 #else
103 (void)data;
105 return (unsigned)x;
106 #endif
107 }
109 /** finds the first character which doesn't fit on the screen */
110 static size_t
111 screen_to_bytes(const gchar *data, unsigned width)
112 {
113 #if defined(HAVE_CURSES_ENHANCED) || defined(ENABLE_MULTIBYTE)
114 size_t length = strlen(data);
115 gchar *dup = g_strdup(data);
116 char *p;
117 unsigned p_width;
119 while (true) {
120 dup[length] = 0;
121 p = locale_to_utf8(dup);
122 p_width = utf8_width(p);
123 g_free(p);
124 if (p_width <= width)
125 break;
127 --length;
128 }
130 g_free(dup);
132 return length;
133 #else
134 (void)data;
136 return (size_t)width;
137 #endif
138 }
140 /** returns the screen column where the cursor is located */
141 static unsigned
142 cursor_column(const struct wreadln *wr)
143 {
144 return byte_to_screen(wr->line + wr->start,
145 wr->cursor - wr->start);
146 }
148 /** returns the offset in the string to align it at the right border
149 of the screen */
150 static inline size_t
151 right_align_bytes(const gchar *data, size_t right, unsigned width)
152 {
153 #if defined(HAVE_CURSES_ENHANCED) || defined(ENABLE_MULTIBYTE)
154 gchar *dup;
155 size_t start = 0;
157 assert(right <= strlen(data));
159 dup = g_strdup(data);
160 dup[right] = 0;
162 while (dup[start] != 0) {
163 char *p = locale_to_utf8(dup + start), *q;
164 unsigned p_width = utf8_width(p);
165 gunichar c;
167 if (p_width < width) {
168 g_free(p);
169 break;
170 }
172 c = g_utf8_get_char(p);
173 p[g_unichar_to_utf8(c, NULL)] = 0;
174 q = utf8_to_locale(p);
175 g_free(p);
177 start += strlen(q);
178 g_free(q);
179 }
181 g_free(dup);
183 return start;
184 #else
185 (void)data;
187 return right >= width ? right + 1 - width : 0;
188 #endif
189 }
191 /** returns the size (in bytes) of the next character */
192 static inline size_t
193 next_char_size(const gchar *data)
194 {
195 #if defined(HAVE_CURSES_ENHANCED) || defined(ENABLE_MULTIBYTE)
196 char *p = locale_to_utf8(data), *q;
197 gunichar c;
198 size_t size;
200 c = g_utf8_get_char(p);
201 p[g_unichar_to_utf8(c, NULL)] = 0;
202 q = utf8_to_locale(p);
203 g_free(p);
205 size = strlen(q);
206 g_free(q);
208 return size;
209 #else
210 (void)data;
212 return 1;
213 #endif
214 }
216 /** returns the size (in bytes) of the previous character */
217 static inline size_t
218 prev_char_size(const gchar *data, size_t x)
219 {
220 #if defined(HAVE_CURSES_ENHANCED) || defined(ENABLE_MULTIBYTE)
221 char *p = locale_to_utf8(data), *q;
222 gunichar c;
223 size_t size;
225 assert(x > 0);
227 q = p;
228 while (true) {
229 c = g_utf8_get_char(q);
230 size = g_unichar_to_utf8(c, NULL);
231 if (size > x)
232 size = x;
233 x -= size;
234 if (x == 0) {
235 g_free(p);
236 return size;
237 }
239 q += size;
240 }
241 #else
242 (void)data;
243 (void)x;
245 return 1;
246 #endif
247 }
249 /* move the cursor one step to the right */
250 static inline void cursor_move_right(struct wreadln *wr)
251 {
252 size_t size;
254 if (wr->line[wr->cursor] == 0)
255 return;
257 size = next_char_size(wr->line + wr->cursor);
258 wr->cursor += size;
259 if (cursor_column(wr) >= wr->width)
260 wr->start = right_align_bytes(wr->line, wr->cursor, wr->width);
261 }
263 /* move the cursor one step to the left */
264 static inline void cursor_move_left(struct wreadln *wr)
265 {
266 size_t size;
268 if (wr->cursor == 0)
269 return;
271 size = prev_char_size(wr->line, wr->cursor);
272 assert(wr->cursor >= size);
273 wr->cursor -= size;
274 if (wr->cursor < wr->start)
275 wr->start = wr->cursor;
276 }
278 /* move the cursor to the end of the line */
279 static inline void cursor_move_to_eol(struct wreadln *wr)
280 {
281 wr->cursor = strlen(wr->line);
282 if (cursor_column(wr) >= wr->width)
283 wr->start = right_align_bytes(wr->line, wr->cursor, wr->width);
284 }
286 /* draw line buffer and update cursor position */
287 static inline void drawline(const struct wreadln *wr)
288 {
289 wmove(wr->w, wr->y, wr->x);
290 /* clear input area */
291 whline(wr->w, ' ', wr->width);
292 /* print visible part of the line buffer */
293 if (wr->masked)
294 whline(wr->w, '*', utf8_width(wr->line + wr->start));
295 else
296 waddnstr(wr->w, wr->line + wr->start,
297 screen_to_bytes(wr->line, wr->width));
298 /* move the cursor to the correct position */
299 wmove(wr->w, wr->y, wr->x + cursor_column(wr));
300 /* tell ncurses to redraw the screen */
301 doupdate();
302 }
304 #if defined(HAVE_CURSES_ENHANCED) || defined(ENABLE_MULTIBYTE)
305 static bool
306 multibyte_is_complete(const char *p, size_t length)
307 {
308 GError *error = NULL;
309 gchar *q = g_locale_to_utf8(p, length,
310 NULL, NULL, &error);
311 if (q != NULL) {
312 g_free(q);
313 return true;
314 } else {
315 g_error_free(error);
316 return false;
317 }
318 }
319 #endif
321 static void
322 wreadln_insert_byte(struct wreadln *wr, gint key)
323 {
324 size_t rest = strlen(wr->line + wr->cursor) + 1;
325 #if (defined(HAVE_CURSES_ENHANCED) || defined(ENABLE_MULTIBYTE)) && !defined (WIN32)
326 char buffer[32] = { key };
327 size_t length = 1;
328 struct pollfd pfd = {
329 .fd = 0,
330 .events = POLLIN,
331 };
332 int ret;
334 /* wide version: try to complete the multibyte sequence */
336 while (length < sizeof(buffer)) {
337 if (multibyte_is_complete(buffer, length))
338 /* sequence is complete */
339 break;
341 /* poll for more bytes on stdin, without timeout */
343 ret = poll(&pfd, 1, 0);
344 if (ret <= 0)
345 /* no more input from keyboard */
346 break;
348 buffer[length++] = wgetch(wr->w);
349 }
351 memmove(wr->line + wr->cursor + length,
352 wr->line + wr->cursor, rest);
353 memcpy(wr->line + wr->cursor, buffer, length);
355 #else
356 const size_t length = 1;
358 memmove(wr->line + wr->cursor + length,
359 wr->line + wr->cursor, rest);
360 wr->line[wr->cursor] = key;
362 #endif
364 wr->cursor += length;
365 if (cursor_column(wr) >= wr->width)
366 wr->start = right_align_bytes(wr->line, wr->cursor, wr->width);
367 }
369 static void
370 wreadln_delete_char(struct wreadln *wr, size_t x)
371 {
372 size_t rest, length;
374 assert(x < strlen(wr->line));
376 length = next_char_size(&wr->line[x]);
377 rest = strlen(&wr->line[x + length]) + 1;
378 memmove(&wr->line[x], &wr->line[x + length], rest);
379 }
381 /* libcurses version */
383 static gchar *
384 _wreadln(WINDOW *w,
385 const gchar *prompt,
386 const gchar *initial_value,
387 unsigned x1,
388 GList **history,
389 GCompletion *gcmp,
390 gboolean masked)
391 {
392 struct wreadln wr = {
393 .w = w,
394 .masked = masked,
395 .cursor = 0,
396 .start = 0,
397 };
398 GList *hlist = NULL, *hcurrent = NULL;
399 gint key = 0;
400 size_t i;
402 #ifdef NCMPC_MINI
403 (void)gcmp;
404 #endif
406 /* turn off echo */
407 noecho();
408 /* make sure the cursor is visible */
409 curs_set(1);
410 /* print prompt string */
411 if (prompt) {
412 waddstr(w, prompt);
413 waddstr(w, ": ");
414 }
415 /* retrieve y and x0 position */
416 getyx(w, wr.y, wr.x);
417 /* check the x1 value */
418 if (x1 <= wr.x || x1 > (unsigned)COLS)
419 x1 = COLS;
420 wr.width = x1 - wr.x;
421 /* clear input area */
422 mvwhline(w, wr.y, wr.x, ' ', wr.width);
424 if (history) {
425 /* append the a new line to our history list */
426 *history = g_list_append(*history, g_malloc0(sizeof(wr.line)));
427 /* hlist points to the current item in the history list */
428 hlist = g_list_last(*history);
429 hcurrent = hlist;
430 }
432 if (initial_value == (char *)-1) {
433 /* get previous history entry */
434 if (history && hlist->prev) {
435 if (hlist == hcurrent)
436 /* save the current line */
437 g_strlcpy(hlist->data, wr.line, sizeof(wr.line));
439 /* get previous line */
440 hlist = hlist->prev;
441 g_strlcpy(wr.line, hlist->data, sizeof(wr.line));
442 }
443 cursor_move_to_eol(&wr);
444 drawline(&wr);
445 } else if (initial_value) {
446 /* copy the initial value to the line buffer */
447 g_strlcpy(wr.line, initial_value, sizeof(wr.line));
448 cursor_move_to_eol(&wr);
449 drawline(&wr);
450 }
452 while (key != 13 && key != '\n') {
453 key = wgetch(w);
455 /* check if key is a function key */
456 for (i = 0; i < 63; i++)
457 if (key == (int)KEY_F(i)) {
458 key = KEY_F(1);
459 i = 64;
460 }
462 switch (key) {
463 #ifdef HAVE_GETMOUSE
464 case KEY_MOUSE: /* ignore mouse events */
465 #endif
466 case ERR: /* ignore errors */
467 break;
469 case TAB:
470 #ifndef NCMPC_MINI
471 if (gcmp) {
472 char *prefix = NULL;
473 GList *list;
475 if (wrln_pre_completion_callback)
476 wrln_pre_completion_callback(gcmp, wr.line,
477 wrln_completion_callback_data);
478 list = g_completion_complete(gcmp, wr.line, &prefix);
479 if (prefix) {
480 g_strlcpy(wr.line, prefix, sizeof(wr.line));
481 cursor_move_to_eol(&wr);
482 g_free(prefix);
483 } else
484 screen_bell();
486 if (wrln_post_completion_callback)
487 wrln_post_completion_callback(gcmp, wr.line, list,
488 wrln_completion_callback_data);
489 }
490 #endif
491 break;
493 case KEY_CTRL_G:
494 screen_bell();
495 if (history) {
496 g_free(hcurrent->data);
497 hcurrent->data = NULL;
498 *history = g_list_delete_link(*history, hcurrent);
499 }
500 return NULL;
502 case KEY_LEFT:
503 case KEY_CTRL_B:
504 cursor_move_left(&wr);
505 break;
506 case KEY_RIGHT:
507 case KEY_CTRL_F:
508 cursor_move_right(&wr);
509 break;
510 case KEY_HOME:
511 case KEY_CTRL_A:
512 wr.cursor = 0;
513 wr.start = 0;
514 break;
515 case KEY_END:
516 case KEY_CTRL_E:
517 cursor_move_to_eol(&wr);
518 break;
519 case KEY_CTRL_K:
520 wr.line[wr.cursor] = 0;
521 break;
522 case KEY_CTRL_U:
523 wr.cursor = utf8_width(wr.line);
524 for (i = 0; i < wr.cursor; i++)
525 wr.line[i] = '\0';
526 wr.cursor = 0;
527 break;
528 case KEY_CTRL_W:
529 /* Firstly remove trailing spaces. */
530 for (i = wr.cursor; i > 0 && wr.line[i-1] == ' '; i--)
531 {
532 cursor_move_left(&wr);
533 wreadln_delete_char(&wr, wr.cursor);
534 }
535 /* Then remove word until next space. */
536 for (; i > 0 && wr.line[i-1] != ' '; i--)
537 {
538 cursor_move_left(&wr);
539 wreadln_delete_char(&wr, wr.cursor);
540 }
541 break;
542 case 127:
543 case KEY_BCKSPC: /* handle backspace: copy all */
544 case KEY_BACKSPACE: /* chars starting from curpos */
545 if (wr.cursor > 0) { /* - 1 from buf[n+1] to buf */
546 cursor_move_left(&wr);
547 wreadln_delete_char(&wr, wr.cursor);
548 }
549 break;
550 case KEY_DC: /* handle delete key. As above */
551 case KEY_CTRL_D:
552 if (wr.line[wr.cursor] != 0)
553 wreadln_delete_char(&wr, wr.cursor);
554 break;
555 case KEY_UP:
556 case KEY_CTRL_P:
557 /* get previous history entry */
558 if (history && hlist->prev) {
559 if (hlist == hcurrent)
560 /* save the current line */
561 g_strlcpy(hlist->data, wr.line,
562 sizeof(wr.line));
564 /* get previous line */
565 hlist = hlist->prev;
566 g_strlcpy(wr.line, hlist->data,
567 sizeof(wr.line));
568 }
569 cursor_move_to_eol(&wr);
570 break;
571 case KEY_DOWN:
572 case KEY_CTRL_N:
573 /* get next history entry */
574 if (history && hlist->next) {
575 /* get next line */
576 hlist = hlist->next;
577 g_strlcpy(wr.line, hlist->data,
578 sizeof(wr.line));
579 }
580 cursor_move_to_eol(&wr);
581 break;
583 case '\n':
584 case 13:
585 case KEY_IC:
586 case KEY_PPAGE:
587 case KEY_NPAGE:
588 case KEY_F(1):
589 /* ignore char */
590 break;
591 default:
592 if (key >= 32)
593 wreadln_insert_byte(&wr, key);
594 }
596 drawline(&wr);
597 }
599 /* update history */
600 if (history) {
601 if (strlen(wr.line)) {
602 /* update the current history entry */
603 size_t size = strlen(wr.line) + 1;
604 hcurrent->data = g_realloc(hcurrent->data, size);
605 g_strlcpy(hcurrent->data, wr.line, size);
606 } else {
607 /* the line was empty - remove the current history entry */
608 g_free(hcurrent->data);
609 hcurrent->data = NULL;
610 *history = g_list_delete_link(*history, hcurrent);
611 }
613 while (g_list_length(*history) > wrln_max_history_length) {
614 GList *first = g_list_first(*history);
616 /* remove the oldest history entry */
617 g_free(first->data);
618 first->data = NULL;
619 *history = g_list_delete_link(*history, first);
620 }
621 }
623 if (wr.line[0] == 0)
624 return NULL;
626 return g_strdup(wr.line);
627 }
629 gchar *
630 wreadln(WINDOW *w,
631 const gchar *prompt,
632 const gchar *initial_value,
633 unsigned x1,
634 GList **history,
635 GCompletion *gcmp)
636 {
637 return _wreadln(w, prompt, initial_value, x1, history, gcmp, FALSE);
638 }
640 gchar *
641 wreadln_masked(WINDOW *w,
642 const gchar *prompt,
643 const gchar *initial_value,
644 unsigned x1,
645 GList **history,
646 GCompletion *gcmp)
647 {
648 return _wreadln(w, prompt, initial_value, x1, history, gcmp, TRUE);
649 }