1 /*
2 * (c) 2004 by Kalle Wallin <kaw@linux.se>
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 * You should have received a copy of the GNU General Public License
14 * along with this program; if not, write to the Free Software
15 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
16 *
17 */
19 #include "wreadln.h"
20 #include "charset.h"
21 #include "screen_utils.h"
22 #include "config.h"
24 #include <assert.h>
25 #include <stdlib.h>
26 #include <string.h>
27 #include <glib.h>
29 #ifdef ENABLE_WIDE
30 #include <sys/poll.h>
31 #endif
33 #define KEY_CTRL_A 1
34 #define KEY_CTRL_B 2
35 #define KEY_CTRL_C 3
36 #define KEY_CTRL_D 4
37 #define KEY_CTRL_E 5
38 #define KEY_CTRL_F 6
39 #define KEY_CTRL_G 7
40 #define KEY_CTRL_K 11
41 #define KEY_CTRL_N 14
42 #define KEY_CTRL_P 16
43 #define KEY_CTRL_U 21
44 #define KEY_CTRL_Z 26
45 #define KEY_BCKSPC 8
46 #define TAB 9
48 struct wreadln {
49 /** the ncurses window where this field is displayed */
50 WINDOW *const w;
52 /** the origin coordinates in the window */
53 unsigned x, y;
55 /** the screen width of the input field */
56 unsigned width;
58 /** is the input masked, i.e. characters displayed as '*'? */
59 const gboolean masked;
61 /** the byte position of the cursor */
62 size_t cursor;
64 /** the byte position displayed at the origin (for horizontal
65 scrolling) */
66 size_t start;
68 /** the current value */
69 gchar line[1024];
70 };
72 /** max items stored in the history list */
73 static const guint wrln_max_history_length = 32;
75 #ifndef NCMPC_MINI
76 void *wrln_completion_callback_data = NULL;
77 wrln_gcmp_pre_cb_t wrln_pre_completion_callback = NULL;
78 wrln_gcmp_post_cb_t wrln_post_completion_callback = NULL;
79 #endif
81 /** converts a byte position to a screen column */
82 static unsigned
83 byte_to_screen(const gchar *data, size_t x)
84 {
85 #ifdef ENABLE_WIDE
86 gchar *dup;
87 char *p;
88 unsigned width;
90 assert(x <= strlen(data));
92 dup = g_strdup(data);
93 dup[x] = 0;
94 p = locale_to_utf8(dup);
95 g_free(dup);
97 width = utf8_width(p);
98 g_free(p);
100 return width;
101 #else
102 (void)data;
104 return (unsigned)x;
105 #endif
106 }
108 /** finds the first character which doesn't fit on the screen */
109 static size_t
110 screen_to_bytes(const gchar *data, unsigned width)
111 {
112 #ifdef ENABLE_WIDE
113 size_t length = strlen(data);
114 gchar *dup = g_strdup(data);
115 char *p;
116 unsigned p_width;
118 while (true) {
119 dup[length] = 0;
120 p = locale_to_utf8(dup);
121 p_width = utf8_width(p);
122 g_free(p);
123 if (p_width <= width)
124 break;
126 --length;
127 }
129 g_free(dup);
131 return length;
132 #else
133 (void)data;
135 return (size_t)width;
136 #endif
137 }
139 /** returns the screen colum where the cursor is located */
140 static unsigned
141 cursor_column(const struct wreadln *wr)
142 {
143 return byte_to_screen(wr->line + wr->start,
144 wr->cursor - wr->start);
145 }
147 /** returns the offset in the string to align it at the right border
148 of the screen */
149 static inline size_t
150 right_align_bytes(const gchar *data, size_t right, unsigned width)
151 {
152 #ifdef ENABLE_WIDE
153 gchar *dup;
154 size_t start = 0;
156 assert(right <= strlen(data));
158 dup = g_strdup(data);
159 dup[right] = 0;
161 while (dup[start] != 0) {
162 char *p = locale_to_utf8(dup + start), *q;
163 unsigned p_width = utf8_width(p);
164 gunichar c;
166 if (p_width < width) {
167 g_free(p);
168 break;
169 }
171 c = g_utf8_get_char(p);
172 p[g_unichar_to_utf8(c, NULL)] = 0;
173 q = utf8_to_locale(p);
174 g_free(p);
176 start += strlen(q);
177 g_free(q);
178 }
180 g_free(dup);
182 return start;
183 #else
184 (void)data;
186 return right >= width ? right + 1 - width : 0;
187 #endif
188 }
190 /** returns the size (in bytes) of the next character */
191 static inline size_t
192 next_char_size(const gchar *data)
193 {
194 #ifdef ENABLE_WIDE
195 char *p = locale_to_utf8(data), *q;
196 gunichar c;
197 size_t size;
199 c = g_utf8_get_char(p);
200 p[g_unichar_to_utf8(c, NULL)] = 0;
201 q = utf8_to_locale(p);
202 g_free(p);
204 size = strlen(q);
205 g_free(q);
207 return size;
208 #else
209 (void)data;
211 return 1;
212 #endif
213 }
215 /** returns the size (in bytes) of the previous character */
216 static inline size_t
217 prev_char_size(const gchar *data, size_t x)
218 {
219 #ifdef ENABLE_WIDE
220 char *p = locale_to_utf8(data), *q;
221 gunichar c;
222 size_t size;
224 assert(x > 0);
226 q = p;
227 while (true) {
228 c = g_utf8_get_char(q);
229 size = g_unichar_to_utf8(c, NULL);
230 if (size > x)
231 size = x;
232 x -= size;
233 if (x == 0) {
234 g_free(p);
235 return size;
236 }
238 q += size;
239 }
240 #else
241 (void)data;
242 (void)x;
244 return 1;
245 #endif
246 }
248 /* move the cursor one step to the right */
249 static inline void cursor_move_right(struct wreadln *wr)
250 {
251 size_t size;
253 if (wr->line[wr->cursor] == 0)
254 return;
256 size = next_char_size(wr->line + wr->cursor);
257 wr->cursor += size;
258 if (cursor_column(wr) >= wr->width)
259 wr->start = right_align_bytes(wr->line, wr->cursor, wr->width);
260 }
262 /* move the cursor one step to the left */
263 static inline void cursor_move_left(struct wreadln *wr)
264 {
265 size_t size;
267 if (wr->cursor == 0)
268 return;
270 size = prev_char_size(wr->line, wr->cursor);
271 assert(wr->cursor >= size);
272 wr->cursor -= size;
273 if (wr->cursor < wr->start)
274 wr->start = wr->cursor;
275 }
277 /* move the cursor to the end of the line */
278 static inline void cursor_move_to_eol(struct wreadln *wr)
279 {
280 wr->cursor = strlen(wr->line);
281 if (cursor_column(wr) >= wr->width)
282 wr->start = right_align_bytes(wr->line, wr->cursor, wr->width);
283 }
285 /* draw line buffer and update cursor position */
286 static inline void drawline(const struct wreadln *wr)
287 {
288 wmove(wr->w, wr->y, wr->x);
289 /* clear input area */
290 whline(wr->w, ' ', wr->width);
291 /* print visible part of the line buffer */
292 if (wr->masked)
293 whline(wr->w, '*', utf8_width(wr->line + wr->start));
294 else
295 waddnstr(wr->w, wr->line + wr->start,
296 screen_to_bytes(wr->line, wr->width));
297 /* move the cursor to the correct position */
298 wmove(wr->w, wr->y, wr->x + cursor_column(wr));
299 /* tell ncurses to redraw the screen */
300 doupdate();
301 }
303 #ifdef ENABLE_WIDE
304 static bool
305 multibyte_is_complete(const char *p, size_t length)
306 {
307 GError *error = NULL;
308 gchar *q = g_locale_to_utf8(p, length,
309 NULL, NULL, &error);
310 if (q != NULL) {
311 g_free(q);
312 return true;
313 } else {
314 g_error_free(error);
315 return false;
316 }
317 }
318 #endif
320 static void
321 wreadln_insert_byte(struct wreadln *wr, gint key)
322 {
323 size_t rest = strlen(wr->line + wr->cursor) + 1;
324 #ifdef ENABLE_WIDE
325 char buffer[32] = { key };
326 size_t length = 1;
327 struct pollfd pfd = {
328 .fd = 0,
329 .events = POLLIN,
330 };
331 int ret;
333 /* wide version: try to complete the multibyte sequence */
335 while (length < sizeof(buffer)) {
336 if (multibyte_is_complete(buffer, length))
337 /* sequence is complete */
338 break;
340 /* poll for more bytes on stdin, without timeout */
342 ret = poll(&pfd, 1, 0);
343 if (ret <= 0)
344 /* no more input from keyboard */
345 break;
347 buffer[length++] = wgetch(wr->w);
348 }
350 memmove(wr->line + wr->cursor + length,
351 wr->line + wr->cursor, rest);
352 memcpy(wr->line + wr->cursor, buffer, length);
354 #else
355 const size_t length = 1;
357 memmove(wr->line + wr->cursor + length,
358 wr->line + wr->cursor, rest);
359 wr->line[wr->cursor] = key;
361 #endif
363 wr->cursor += length;
364 if (cursor_column(wr) >= wr->width)
365 wr->start = right_align_bytes(wr->line, wr->cursor, wr->width);
366 }
368 static void
369 wreadln_delete_char(struct wreadln *wr, size_t x)
370 {
371 size_t rest, length;
373 assert(x < strlen(wr->line));
375 length = next_char_size(&wr->line[x]);
376 rest = strlen(&wr->line[x + length]) + 1;
377 memmove(&wr->line[x], &wr->line[x + length], rest);
378 }
380 /* libcurses version */
382 static gchar *
383 _wreadln(WINDOW *w,
384 const gchar *prompt,
385 const gchar *initial_value,
386 unsigned x1,
387 GList **history,
388 GCompletion *gcmp,
389 gboolean masked)
390 {
391 struct wreadln wr = {
392 .w = w,
393 .masked = masked,
394 .cursor = 0,
395 .start = 0,
396 };
397 GList *hlist = NULL, *hcurrent = NULL;
398 gint key = 0;
399 size_t i;
401 #ifdef NCMPC_MINI
402 (void)gcmp;
403 #endif
405 /* turn off echo */
406 noecho();
407 /* make shure the cursor is visible */
408 curs_set(1);
409 /* print prompt string */
410 if (prompt)
411 waddstr(w, prompt);
412 /* retrive y and x0 position */
413 getyx(w, wr.y, wr.x);
414 /* check the x1 value */
415 if (x1 <= wr.x || x1 > (unsigned)COLS)
416 x1 = COLS;
417 wr.width = x1 - wr.x;
418 /* clear input area */
419 mvwhline(w, wr.y, wr.x, ' ', wr.width);
421 if (history) {
422 /* append the a new line to our history list */
423 *history = g_list_append(*history, g_malloc0(sizeof(wr.line)));
424 /* hlist points to the current item in the history list */
425 hlist = g_list_last(*history);
426 hcurrent = hlist;
427 }
429 if (initial_value == (char *)-1) {
430 /* get previous history entry */
431 if (history && hlist->prev) {
432 if (hlist == hcurrent)
433 /* save the current line */
434 g_strlcpy(hlist->data, wr.line, sizeof(wr.line));
436 /* get previous line */
437 hlist = hlist->prev;
438 g_strlcpy(wr.line, hlist->data, sizeof(wr.line));
439 }
440 cursor_move_to_eol(&wr);
441 drawline(&wr);
442 } else if (initial_value) {
443 /* copy the initial value to the line buffer */
444 g_strlcpy(wr.line, initial_value, sizeof(wr.line));
445 cursor_move_to_eol(&wr);
446 drawline(&wr);
447 }
449 while (key != 13 && key != '\n') {
450 key = wgetch(w);
452 /* check if key is a function key */
453 for (i = 0; i < 63; i++)
454 if (key == (int)KEY_F(i)) {
455 key = KEY_F(1);
456 i = 64;
457 }
459 switch (key) {
460 #ifdef HAVE_GETMOUSE
461 case KEY_MOUSE: /* ignore mouse events */
462 #endif
463 case ERR: /* ingnore errors */
464 break;
466 case TAB:
467 #ifndef NCMPC_MINI
468 if (gcmp) {
469 char *prefix = NULL;
470 GList *list;
472 if (wrln_pre_completion_callback)
473 wrln_pre_completion_callback(gcmp, wr.line,
474 wrln_completion_callback_data);
475 list = g_completion_complete(gcmp, wr.line, &prefix);
476 if (prefix) {
477 g_strlcpy(wr.line, prefix, sizeof(wr.line));
478 cursor_move_to_eol(&wr);
479 g_free(prefix);
480 } else
481 screen_bell();
483 if (wrln_post_completion_callback)
484 wrln_post_completion_callback(gcmp, wr.line, list,
485 wrln_completion_callback_data);
486 }
487 #endif
488 break;
490 case KEY_CTRL_G:
491 screen_bell();
492 if (history) {
493 g_free(hcurrent->data);
494 hcurrent->data = NULL;
495 *history = g_list_delete_link(*history, hcurrent);
496 }
497 return NULL;
499 case KEY_LEFT:
500 case KEY_CTRL_B:
501 cursor_move_left(&wr);
502 break;
503 case KEY_RIGHT:
504 case KEY_CTRL_F:
505 cursor_move_right(&wr);
506 break;
507 case KEY_HOME:
508 case KEY_CTRL_A:
509 wr.cursor = 0;
510 wr.start = 0;
511 break;
512 case KEY_END:
513 case KEY_CTRL_E:
514 cursor_move_to_eol(&wr);
515 break;
516 case KEY_CTRL_K:
517 wr.line[wr.cursor] = 0;
518 break;
519 case KEY_CTRL_U:
520 wr.cursor = utf8_width(wr.line);
521 for (i = 0; i < wr.cursor; i++)
522 wr.line[i] = '\0';
523 wr.cursor = 0;
524 break;
525 case 127:
526 case KEY_BCKSPC: /* handle backspace: copy all */
527 case KEY_BACKSPACE: /* chars starting from curpos */
528 if (wr.cursor > 0) { /* - 1 from buf[n+1] to buf */
529 cursor_move_left(&wr);
530 wreadln_delete_char(&wr, wr.cursor);
531 }
532 break;
533 case KEY_DC: /* handle delete key. As above */
534 case KEY_CTRL_D:
535 if (wr.line[wr.cursor] != 0)
536 wreadln_delete_char(&wr, wr.cursor);
537 break;
538 case KEY_UP:
539 case KEY_CTRL_P:
540 /* get previous history entry */
541 if (history && hlist->prev) {
542 if (hlist == hcurrent)
543 /* save the current line */
544 g_strlcpy(hlist->data, wr.line,
545 sizeof(wr.line));
547 /* get previous line */
548 hlist = hlist->prev;
549 g_strlcpy(wr.line, hlist->data,
550 sizeof(wr.line));
551 }
552 cursor_move_to_eol(&wr);
553 break;
554 case KEY_DOWN:
555 case KEY_CTRL_N:
556 /* get next history entry */
557 if (history && hlist->next) {
558 /* get next line */
559 hlist = hlist->next;
560 g_strlcpy(wr.line, hlist->data,
561 sizeof(wr.line));
562 }
563 cursor_move_to_eol(&wr);
564 break;
566 case '\n':
567 case 13:
568 case KEY_IC:
569 case KEY_PPAGE:
570 case KEY_NPAGE:
571 case KEY_F(1):
572 /* ignore char */
573 break;
574 default:
575 if (key >= 32)
576 wreadln_insert_byte(&wr, key);
577 }
579 drawline(&wr);
580 }
582 /* update history */
583 if (history) {
584 if (strlen(wr.line)) {
585 /* update the current history entry */
586 size_t size = strlen(wr.line) + 1;
587 hcurrent->data = g_realloc(hcurrent->data, size);
588 g_strlcpy(hcurrent->data, wr.line, size);
589 } else {
590 /* the line was empty - remove the current history entry */
591 g_free(hcurrent->data);
592 hcurrent->data = NULL;
593 *history = g_list_delete_link(*history, hcurrent);
594 }
596 while (g_list_length(*history) > wrln_max_history_length) {
597 GList *first = g_list_first(*history);
599 /* remove the oldest history entry */
600 g_free(first->data);
601 first->data = NULL;
602 *history = g_list_delete_link(*history, first);
603 }
604 }
606 if (wr.line[0] == 0)
607 return NULL;
609 return g_strdup(wr.line);
610 }
612 gchar *
613 wreadln(WINDOW *w,
614 const gchar *prompt,
615 const gchar *initial_value,
616 unsigned x1,
617 GList **history,
618 GCompletion *gcmp)
619 {
620 return _wreadln(w, prompt, initial_value, x1, history, gcmp, FALSE);
621 }
623 gchar *
624 wreadln_masked(WINDOW *w,
625 const gchar *prompt,
626 const gchar *initial_value,
627 unsigned x1,
628 GList **history,
629 GCompletion *gcmp)
630 {
631 return _wreadln(w, prompt, initial_value, x1, history, gcmp, TRUE);
632 }