]> git.deb.at Git - pkg/abook.git/blob - ui.c
Upload 0.6.1-2 to unstable
[pkg/abook.git] / ui.c
1
2 /*
3  * $Id$
4  *
5  * by JH <jheinonen@users.sourceforge.net>
6  *
7  * Copyright (C) Jaakko Heinonen
8  */
9
10 #include <stdio.h>
11 #include <stdlib.h>
12 #include <string.h>
13 #include <unistd.h>
14 #include <fcntl.h>
15 #include <signal.h>
16 #include <ctype.h>
17 #include "abook.h"
18 #include <assert.h>
19 #include "ui.h"
20 #include "edit.h"
21 #include "database.h"
22 #include "gettext.h"
23 #include "list.h"
24 #include "misc.h"
25 #include "options.h"
26 #include "filter.h"
27 #include "xmalloc.h"
28 #include "color.h"
29 #include <sys/time.h>
30 #ifdef HAVE_CONFIG_H
31 #       include "config.h"
32 #endif
33 #ifdef HAVE_SYS_IOCTL_H
34 #       include <sys/ioctl.h>
35 #endif
36
37 #include "abook_rl.h"
38
39 /*
40  * external variables
41  */
42
43 extern char *datafile;
44
45 extern bool alternative_datafile;
46
47 /*
48  * internal variables
49  */
50
51 static bool ui_initialized = FALSE;
52
53 static bool should_resize = FALSE;
54 static bool can_resize = FALSE;
55 static struct timeval last_click_time;
56 static int double_click_interval = 200; /* maximum time in milliseconds */
57
58 static WINDOW *top = NULL, *bottom = NULL;
59
60
61 static void
62 init_windows()
63 {
64         top = newwin(LIST_TOP - 1, COLS, 0, 0);
65         bottom = newwin(LINES - LIST_BOTTOM, COLS, LIST_BOTTOM, 0);
66 }
67
68 static void
69 free_windows()
70 {
71         delwin(top);
72         delwin(bottom);
73 }
74
75
76 #ifdef SIGWINCH
77 static void
78 resize_abook()
79 {
80 #ifdef TIOCGWINSZ
81         struct winsize winsz;
82
83         ioctl(0, TIOCGWINSZ, &winsz);
84 #ifdef DEBUG
85         if(winsz.ws_col >= MIN_COLS && winsz.ws_row >= MIN_LINES) {
86                 fprintf(stderr, "Warning: COLS=%d, LINES=%d\n", winsz.ws_col, winsz.ws_row);
87         }
88 #endif
89
90         if(winsz.ws_col >= MIN_COLS && winsz.ws_row >= MIN_LINES) {
91 #ifdef HAVE_RESIZETERM
92                 resizeterm(winsz.ws_row, winsz.ws_col);
93 #else
94                 COLS = winsz.ws_col;
95                 LINES = winsz.ws_row;
96 #endif
97         }
98
99         should_resize = FALSE;
100         close_list(); /* we need to recreate windows */
101         init_list();
102         free_windows();
103         init_windows();
104         refresh_screen();
105         refresh();
106 #endif /* TIOCGWINSZ */
107 }
108
109
110 static void
111 win_changed(int i)
112 {
113         if(can_resize)
114                 resize_abook();
115         else
116                 should_resize = TRUE;
117 }
118 #endif /* SIGWINCH */
119
120
121 int
122 is_ui_initialized()
123 {
124         return ui_initialized;
125 }
126
127 void
128 ui_init_curses()
129 {
130         if(!is_ui_initialized())
131                 initscr();
132         cbreak();
133         noecho();
134         nonl();
135         intrflush(stdscr, FALSE);
136         if(opt_get_bool(BOOL_USE_MOUSE)) {
137                 mouseinterval(0);
138                 timerclear(&last_click_time);
139                 ui_enable_mouse(TRUE);
140         }
141         keypad(stdscr, TRUE);
142         if(opt_get_bool(BOOL_USE_COLORS) && has_colors()) {
143                 start_color();
144                 use_default_colors();
145                 ui_init_color_pairs_user();
146         }
147 }
148
149 void
150 ui_enable_mouse(bool enabled)
151 {
152         mmask_t mask;
153         if(enabled) {
154                 mask = BUTTON1_CLICKED | BUTTON4_PRESSED;
155 #if NCURSES_MOUSE_VERSION == 2
156                 mask |= BUTTON5_PRESSED;
157 #endif
158         } else {
159                 mask = 0;
160         }
161         mousemask(mask, NULL);
162 }
163
164 /** Check the time elapsed since last click and tell if it should be
165  * interpreted as a double click
166  */
167 static bool
168 was_double_click() {
169         struct timeval click_time, click_diff, maxdiff;
170         maxdiff.tv_sec = double_click_interval / 1000;
171         maxdiff.tv_usec = (double_click_interval % 1000)*1000;
172         gettimeofday(&click_time, NULL);
173
174         timersub(&click_time, &last_click_time, &click_diff);
175         last_click_time = click_time;
176         return !timercmp(&click_diff, &maxdiff, >);
177 }
178
179 #define CHECK_COLOR_NAME(value, name, DEFNAME) \
180         if(!strcmp((name), (value))){ \
181                 return DEFNAME; \
182         }
183 short
184 opt_color_to_color(enum str_opts enum_name)
185 {
186         char* name = opt_get_str(enum_name);
187         CHECK_COLOR_NAME(name, "default", COLOR_DEFAULT)
188         else CHECK_COLOR_NAME(name, "black", COLOR_BLACK)
189         else CHECK_COLOR_NAME(name, "red", COLOR_RED)
190         else CHECK_COLOR_NAME(name, "green", COLOR_GREEN)
191         else CHECK_COLOR_NAME(name, "yellow", COLOR_YELLOW)
192         else CHECK_COLOR_NAME(name, "blue", COLOR_BLUE)
193         else CHECK_COLOR_NAME(name, "magenta", COLOR_MAGENTA)
194         else CHECK_COLOR_NAME(name, "cyan", COLOR_CYAN)
195         else CHECK_COLOR_NAME(name, "white", COLOR_WHITE)
196     else return COLOR_DEFAULT;
197 }
198
199 void
200 ui_init_color_pairs_user()
201 {
202         init_pair(CP_HEADER, opt_color_to_color(STR_COLOR_HEADER_FG),
203                              opt_color_to_color(STR_COLOR_HEADER_BG));
204         init_pair(CP_FOOTER, opt_color_to_color(STR_COLOR_FOOTER_FG),
205                              opt_color_to_color(STR_COLOR_FOOTER_BG));
206         init_pair(CP_LIST_EVEN, opt_color_to_color(STR_COLOR_LIST_EVEN_FG),
207                                 opt_color_to_color(STR_COLOR_LIST_EVEN_BG));
208         init_pair(CP_LIST_ODD,  opt_color_to_color(STR_COLOR_LIST_ODD_FG),
209                                 opt_color_to_color(STR_COLOR_LIST_ODD_BG));
210         init_pair(CP_LIST_HEADER, opt_color_to_color(STR_COLOR_LIST_HEADER_FG),
211                              opt_color_to_color(STR_COLOR_LIST_HEADER_BG));
212         init_pair(CP_LIST_HIGHLIGHT, opt_color_to_color(STR_COLOR_LIST_HIGHLIGHT_FG),
213                              opt_color_to_color(STR_COLOR_LIST_HIGHLIGHT_BG));
214         init_pair(CP_TAB_BORDER, opt_color_to_color(STR_COLOR_TAB_BORDER_FG),
215                              opt_color_to_color(STR_COLOR_TAB_BORDER_BG));
216         init_pair(CP_TAB_LABEL, opt_color_to_color(STR_COLOR_TAB_LABEL_FG),
217                              opt_color_to_color(STR_COLOR_TAB_LABEL_BG));
218         init_pair(CP_FIELD_NAME, opt_color_to_color(STR_COLOR_FIELD_NAME_FG),
219                              opt_color_to_color(STR_COLOR_FIELD_NAME_BG));
220         init_pair(CP_FIELD_VALUE, opt_color_to_color(STR_COLOR_FIELD_VALUE_FG),
221                              opt_color_to_color(STR_COLOR_FIELD_VALUE_BG));
222 }
223
224 int
225 init_ui()
226 {
227         ui_init_curses();
228 #ifdef DEBUG
229         fprintf(stderr, "init_abook():\n");
230         fprintf(stderr, "  COLS = %d, LINES = %d\n", COLS, LINES);
231 #endif
232         if( LINES < MIN_LINES || COLS < MIN_COLS ) {
233                 clear(); refresh(); endwin();
234                 fprintf(stderr, _("Your terminal size is %dx%d\n"), COLS, LINES);
235                 fprintf(stderr, _("Terminal is too small. Minimum terminal "
236                                 "size for abook is "
237                                 "%dx%d\n"), MIN_COLS, MIN_LINES);
238                 return 1;
239         }
240
241         init_list();
242         init_windows();
243
244         ui_initialized = TRUE;
245
246 #ifdef SIGWINCH
247         signal(SIGWINCH, win_changed);
248 #endif
249
250         return 0;
251 }
252
253 void
254 close_ui()
255 {
256         close_list();
257         free_windows();
258         clear();
259         refresh();
260         endwin();
261
262         ui_initialized = FALSE;
263 }
264
265
266 void
267 headerline(const char *str)
268 {
269         werase(top);
270
271         wattrset(top, COLOR_PAIR(CP_HEADER));
272         mvwhline(top, 0, 0, ' ', COLS);
273         mvwhline(top, 1, 0, UI_HLINE_CHAR, COLS);
274
275         mvwprintw(top, 0, 0, "%s | %s", PACKAGE " " VERSION, str);
276
277         refresh();
278         wrefresh(top);
279 }
280
281
282 void
283 refresh_screen()
284 {
285 #ifdef SIGWINCH
286         if(should_resize) {
287                 resize_abook();
288                 return;
289         }
290 #endif
291         clear();
292
293         refresh_statusline();
294         headerline(gettext(MAIN_HELPLINE));
295         list_headerline();
296
297         refresh_list();
298 }
299
300
301 int
302 statusline_msg(const char *msg)
303 {
304         int c;
305
306         clear_statusline();
307         statusline_addstr(msg);
308         c = getch();
309 #ifdef DEBUG
310         fprintf(stderr, "statusline_msg(\"%s\")\n", msg);
311 #endif
312         clear_statusline();
313
314         return c;
315 }
316
317 void
318 statusline_addstr(const char *str)
319 {
320         mvwaddstr(bottom, 1, 0, str);
321         refresh();
322         wrefresh(bottom);
323 }
324
325 /* Same as statusline_addstr(), but hilight "<str>" sequences if the terminal
326  * supports it */
327 static void
328 statusline_addhlstr(const char *str)
329 {
330 #if defined(A_BOLD) && defined(A_NORMAL) && defined(A_DIM)
331         const char *p = str, *start = str;
332         char *tmp;
333         int pos = 0;
334
335         while(1) {
336                 if(!*p || strchr("<>", *p)) {
337                         if(p - start > 0) {
338                                 wattrset(bottom, (*p == '>') ? A_BOLD : A_NORMAL);
339                                 tmp = xstrndup(start, p - start);
340                                 mvwaddstr(bottom, 1, pos, tmp);
341                                 pos += strwidth(tmp);
342                                 free(tmp);
343                         }
344                         if(*p) {
345                                 start = p + 1;
346
347                                 /* show tag markers */
348                                 wattrset(bottom, A_DIM);
349                                 mvwaddch(bottom, 1, pos++, *p);
350                         }
351                 }
352
353                 if(!*p) {
354                         wattrset(bottom, A_NORMAL);
355                         break;
356                 }
357
358                 p++;
359         }
360 #else
361         mvwaddstr(bottom, 1, 0, str);
362 #endif
363
364         refresh();
365         wrefresh(bottom);
366 }
367
368 int
369 statusline_askchoice(const char *msg, const char *choices, short dflt)
370 {
371         char *s;
372         int ch;
373
374         assert((dflt >= 0) && (dflt <= strlen(choices)));
375
376         if(dflt) {
377                 s = strdup_printf("%s [%c]", msg, choices[dflt - 1]);
378                 statusline_addhlstr(s);
379                 free(s);
380         } else
381                 statusline_addhlstr(msg);
382
383         while(1)
384         {
385                 ch = tolower(getch());
386
387                 if(ch == 7) /* ctrl+G */
388                         return 0;
389
390                 if(dflt && (ch == '\r')) /* default choice */
391                         return dflt;
392
393                 if((s = strchr(choices, ch)))
394                         return (s - choices + 1);
395         }
396 }
397
398 char *
399 ui_readline(const char *prompt, char *s, size_t limit, bool use_completion)
400 {
401         int y, x;
402         char *ret;
403
404         mvwaddstr(bottom, 1, 0, prompt);
405
406         getyx(bottom, y, x);
407
408         ret = abook_readline(bottom, y, x, s, use_completion);
409
410         if(ret) {
411                 strtrim(ret);
412                 if(strlen(ret) > limit && limit > 0)
413                         ret[limit] = '\0';
414         }
415
416         return ret;
417 }
418
419 int
420 statusline_ask_boolean(const char *msg, int def)
421 {
422         int ret;
423         char *msg2 = strconcat(msg,  def ? _(" (Y/n)?") : _(" (y/N)?"), NULL);
424         char ch;
425
426         statusline_addstr(msg2);
427
428         free(msg2);
429
430         ch = tolower(getch());
431
432         if(ch == *(S_("keybinding for no|n")))
433                 ret = FALSE;
434         else if(ch == *(S_("keybinding for yes|y")))
435                 ret = TRUE;
436         else
437                 ret = def;
438
439         clear_statusline();
440
441         return ret;
442 }
443
444 void
445 refresh_statusline()
446 {
447         werase(bottom);
448
449         wattrset(bottom, COLOR_PAIR(CP_FOOTER));
450         mvwhline(bottom, 0, 0, UI_HLINE_CHAR, COLS);
451
452         refresh();
453         wrefresh(bottom);
454 }
455
456 char *
457 ask_filename(const char *prompt)
458 {
459         char *buf = NULL;
460
461         clear_statusline();
462
463         buf = ui_readline(prompt, NULL, -1, 1);
464
465         return buf;
466 }
467
468 void
469 clear_statusline()
470 {
471         wmove(bottom, 1, 0);
472         wclrtoeol(bottom);
473         wrefresh(bottom);
474         refresh();
475 }
476
477 /*
478  * help
479  */
480
481 #include "help.h"
482
483 void
484 display_help(int help)
485 {
486         int i;
487         char **tbl;
488         WINDOW *helpw;
489
490         switch(help) {
491                 case HELP_MAIN:
492                         tbl = mainhelp;
493                         break;
494                 case HELP_EDITOR:
495                         tbl = editorhelp;
496                         break;
497                 default:return;
498         }
499
500         helpw = newwin(LINES - 5, COLS - 6, 2, 3);
501         erase();
502         headerline(_("help"));
503
504         for(i = 0; tbl[i] != NULL; i++) {
505                 waddstr(helpw, gettext(tbl[i]));
506                 if( (!((i + 1) % (LINES - 8))) ||
507                         (tbl[i + 1] == NULL) ) {
508                         refresh();
509                         wrefresh(helpw);
510                         refresh_statusline();
511                         if(statusline_msg(_("Press any key to continue..."))
512                                         == 'q')
513                                 break;
514                         wclear(helpw);
515                 }
516         }
517
518         clear_statusline();
519         delwin(helpw);
520 }
521
522 /*
523  * end of help
524  */
525
526 extern char *selected;
527
528 void
529 get_commands()
530 {
531         int ch;
532
533         for(;;) {
534                 can_resize = TRUE; /* it's safe to resize now */
535                 if(!opt_get_bool(BOOL_SHOW_CURSOR))
536                         hide_cursor();
537                 if(should_resize)
538                         refresh_screen();
539                 ch = getch();
540                 if(!opt_get_bool(BOOL_SHOW_CURSOR))
541                         show_cursor();
542                 can_resize = FALSE; /* it's not safe to resize anymore */
543                 if(ch == KEY_MOUSE) {
544                         MEVENT event;
545                         bool double_clicked = was_double_click();
546                         if(getmouse(&event) == OK) {
547                                 if(event.bstate & BUTTON1_CLICKED
548                                    || event.bstate & BUTTON1_DOUBLE_CLICKED) {
549                                         if(event.y == 0) {
550                                                 return;
551                                         }
552                                         list_set_curitem(event.y + list_get_firstitem() - LIST_TOP);
553                                         if(double_clicked) {
554                                                 edit_item(-1);
555                                         } else {
556                                                 refresh_list();
557                                         }
558                                 } else if(event.bstate & BUTTON4_PRESSED) {
559                                         scroll_list_up();
560                                 } else if(event.bstate & BUTTON5_PRESSED) {
561                                         scroll_list_down();
562                                 }
563                         }
564                 }
565                 switch(ch) {
566                         case 'q': return;
567                         case 'Q': quit_abook(QUIT_DONTSAVE);    break;
568                         case 'P': print_stderr(selected_items() ?
569                                                   -1 : list_get_curitem());
570                                   return;
571                         case '?':
572                                   display_help(HELP_MAIN);
573                                   refresh_screen();
574                                   break;
575                         case 'a': add_item();           break;
576                         case '\r': edit_item(-1);       break;
577                         case KEY_DC:
578                         case 'd':
579                         case 'r': ui_remove_items();    break;
580                         case 'M': ui_merge_items();     break;
581                         case 'D': duplicate_item();     break;
582                         case 'U': ui_remove_duplicates(); break;
583                         case 12: refresh_screen();      break;
584
585                         case 'k':
586                         case KEY_UP: scroll_up();       break;
587                         case 'j':
588                         case KEY_DOWN: scroll_down();   break;
589                         case 'K':
590                         case KEY_PPAGE: page_up();      break;
591                         case 'J':
592                         case KEY_NPAGE: page_down();    break;
593
594                         case 'g':
595                         case KEY_HOME: goto_home();     break;
596                         case 'G':
597                         case KEY_END: goto_end();       break;
598
599                         case 'w': save_database();
600                                   break;
601                         case 'l': ui_read_database();   break;
602                         case 'i': import_database();    break;
603                         case 'e': export_database();    break;
604                         case 'C': ui_clear_database();  break;
605
606                         case 'o': ui_open_datafile();   break;
607
608                         case 's': sort_by_field("name");break;
609                         case 'S': sort_surname();       break;
610                         case 'F': sort_by_field(NULL);  break;
611
612                         case '/': ui_find(0);           break;
613                         case 'n':
614                         case '\\': ui_find(1);          break;
615
616                         case ' ': if(list_get_curitem() >= 0) {
617                                    list_invert_curitem_selection();
618                                    ui_print_number_of_items();
619                                    refresh_list();
620                                   }
621                                 break;
622                         case '+': select_all();
623                                   refresh_list();
624                                 break;
625                         case '-': select_none();
626                                   refresh_list();
627                                 break;
628                         case '*': invert_selection();
629                                   refresh_list();
630                                  break;
631                         case 'A': move_curitem(MOVE_ITEM_UP);
632                                 break;
633                         case 'Z': move_curitem(MOVE_ITEM_DOWN);
634                                 break;
635
636                         case 'm': launch_mutt(selected_items() ?
637                                                   -1 : list_get_curitem());
638                                   refresh_screen();
639                                   break;
640
641                         case 'p': ui_print_database(); break;
642
643                         case 'v': launch_wwwbrowser(list_get_curitem());
644                                   refresh_screen();
645                                   break;
646                 }
647         }
648 }
649
650 void
651 ui_remove_items()
652 {
653         if(list_is_empty())
654                 return;
655
656         if(statusline_ask_boolean(_("Remove selected item(s)"), FALSE))
657                 remove_selected_items();
658
659         clear_statusline();
660         refresh_list();
661 }
662
663 void
664 ui_merge_items()
665 {
666         if(statusline_ask_boolean(_("Merge selected items"), FALSE))
667                 merge_selected_items();
668
669         clear_statusline();
670         refresh_list();
671 }
672
673 void            ui_remove_duplicates()
674 {
675         if(statusline_ask_boolean(_("Remove duplicates"), FALSE))
676                 remove_duplicates();
677         
678         clear_statusline();
679         refresh_list();
680 }
681
682 void
683 ui_clear_database()
684 {
685         if(statusline_ask_boolean(_("Clear WHOLE database"), FALSE)) {
686                 close_database();
687                 refresh_list();
688         }
689 }
690
691 void
692 ui_find(int next)
693 {
694         int item = -1;
695         static char findstr[MAX_FIELD_LEN];
696         int search_fields[] = {NAME, EMAIL, NICK, -1};
697
698         clear_statusline();
699
700         if(next) {
701                 if(!*findstr)
702                         return;
703         } else {
704                 char *s;
705                 s = ui_readline("/", findstr, MAX_FIELD_LEN - 1, 0);
706                 refresh_screen();
707                 if(s == NULL) {
708                         return; /* user cancelled (ctrl-G) */
709                 } else {
710                         strncpy(findstr, s, MAX_FIELD_LEN);
711                         free(s);
712                 }
713         }
714
715         if( (item = find_item(findstr, list_get_curitem() + !!next,
716                         search_fields)) < 0 &&
717                         (item = find_item(findstr, 0, search_fields)) >= 0)
718                 statusline_addstr(_("Search hit bottom, continuing at top"));
719
720         if(item >= 0) {
721                 list_set_curitem(item);
722                 refresh_list();
723         }
724 }
725
726 void
727 ui_print_number_of_items()
728 {
729         char *str = strdup_printf("     " "|%3d/%3d",
730                 selected_items(), db_n_items());
731
732         attrset(COLOR_PAIR(CP_HEADER));
733         mvaddstr(0, COLS-strlen(str), str);
734
735         free(str);
736 }
737
738 void
739 ui_read_database()
740 {
741         char *msg;
742
743         if(!list_is_empty()) {
744                 msg = strdup_printf(_("Your current data will be lost - "
745                                 "Press '%c' to continue"),
746                                 *(S_("keybinding for yes|y")));
747                 if(!statusline_ask_boolean(msg, FALSE)) {
748                         free(msg);
749                         return;
750                 }
751                 free(msg);
752         }
753
754         load_database(datafile);
755         refresh_list();
756 }
757
758
759 void
760 ui_print_database()
761 {
762         FILE *handle;
763         char *command = opt_get_str(STR_PRINT_COMMAND);
764         int mode;
765
766         if(list_is_empty())
767                 return;
768
769         switch(statusline_askchoice(_("Print <a>ll, print <s>elected, or <c>ancel?"), S_("keybindings:all/selected/cancel|asc"), 3)) {
770                 case 1:
771                         mode = ENUM_ALL;
772                         break;
773                 case 2:
774                         if( !selected_items() ) {
775                                 statusline_msg(_("No selected items"));
776                                 return;
777                         }
778                         mode = ENUM_SELECTED;
779                         break;
780                 default:
781                         refresh_screen();
782                         return;
783         }
784
785         clear_statusline();
786
787         if( ! *command || (handle = popen(command, "w")) == NULL)
788                 return;
789
790         fexport("text", handle, mode);
791
792         pclose(handle);
793 }
794
795
796 void
797 ui_open_datafile()
798 {
799         char *filename;
800
801         filename = ask_filename(_("File to open: "));
802
803         if(!filename || ! *filename) {
804                 free(filename);
805                 refresh_screen();
806                 return;
807         }
808
809         if(opt_get_bool(BOOL_AUTOSAVE))
810                 save_database();
811         else if(statusline_ask_boolean(_("Save current database"), FALSE))
812                 save_database();
813
814         close_database();
815
816         load_database(filename);
817
818         if(list_is_empty()) {
819                 statusline_msg(_("Sorry, the specified file appears not to be a valid abook addressbook"));
820                 load_database(datafile);
821         } else {
822                 free(datafile);
823                 datafile = xstrdup(filename);
824         }
825
826         refresh_screen();
827         free(filename);
828
829         alternative_datafile = TRUE;
830 }