1 /*
2 * ckpass.c
3 *
4 * Main program and support functions.
5 *
6 * This file is part of the ckpass project.
7 *
8 * Copyright (C) 2009 Heath N. Caldwell <hncaldwell@gmail.com>
9 *
10 * This program is free software: you can redistribute it and/or modify
11 * it under the terms of the GNU General Public License as published by
12 * the Free Software Foundation, either version 3 of the License, or
13 * (at your option) any later version.
14 *
15 * This program is distributed in the hope that it will be useful,
16 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 * GNU General Public License for more details.
19 *
20 * You should have received a copy of the GNU General Public License
21 * along with this program. If not, see <http://www.gnu.org/licenses/>.
22 *
23 */
24
25 #include <ncurses.h>
26 #include <locale.h>
27
28 #include <stdlib.h>
29 #include <stdio.h>
30 #include <string.h>
31 #include <sys/mman.h>
32 #include <sys/stat.h>
33 #include <unistd.h>
34 #include <fcntl.h>
35 #include <errno.h>
36
37 #include <kpass.h>
38
39 #include "ckpass.h"
40 #include "bindings.h"
41 #include "forms.h"
42
main(int argc,char ** argv)43 int main(int argc, char **argv)
44 {
45 kpass_db *db;
46 kpass_db *new_db;
47 int press;
48 int active_win;
49 int first_group = 0;
50 int highlighted_group = 0;
51 int selected_group = 0;
52 int entries_in_selected_group = 0;
53 int first_entry = 0;
54 int highlighted_entry = 0;
55 int max_x, max_y;
56 bool reveal = FALSE;
57 char db_filename[MAX_FILENAME_LENGTH];
58 char new_db_filename[MAX_FILENAME_LENGTH];
59 struct binding *b;
60 struct kpass_entry *entry;
61 bool file_from_arg = FALSE; /* True if using database filename from command line argument. */
62 int result;
63
64 setlocale(LC_ALL, "C");
65
66 initscr();
67 start_color();
68 cbreak();
69 noecho();
70 //nonl();
71 //intrflush(stdscr, FALSE);
72 curs_set(0);
73 keypad(stdscr, TRUE);
74
75 active_win = WIN_GROUPS;
76 init_bindings();
77
78 db = malloc(sizeof(kpass_db));
79
80 if(argc > 1) {
81 strncpy(db_filename, argv[1], MAX_FILENAME_LENGTH);
82 file_from_arg = TRUE;
83 }
84
85 do {
86 /* If it was set by command line argument, try it the first time. If
87 * the user cancels password prompt, read a new filename with form. */
88 if(!file_from_arg) {
89 if(open_database_form(db_filename, MAX_FILENAME_LENGTH)) goto quit;
90 }
91
92 file_from_arg = FALSE;
93
94 result = open_db(db, db_filename);
95 if(result < 0) kpass_free_db(db); /* It was initialized, but password cancelled. */
96 } while(result);
97
98
99
100 init_windows(db);
101 draw_groups(db, first_group, highlighted_group, TRUE);
102 draw_entries(db, highlighted_group, first_entry, highlighted_entry, FALSE, FALSE);
103 draw_top_bar(db_filename);
104 draw_bottom_bar(active_win);
105
106 while(1) {
107 press = wgetch(active_win == WIN_GROUPS ? _groups_win : _entries_win);
108
109 if(press == KEY_RESIZE) {
110 init_windows(db);
111 draw_groups(db, first_group, highlighted_group, active_win == WIN_GROUPS ? TRUE : FALSE);
112 draw_entries(db, highlighted_group, first_entry, highlighted_entry, active_win == WIN_ENTRIES ? TRUE : FALSE, FALSE);
113 draw_bottom_bar(active_win);
114 continue;
115 }
116
117 if(active_win == WIN_GROUPS) {
118 for(b = _groups_bindings; b->key && b->key != press; b++);
119 if(!b->key) continue;
120
121 if(b->command == C_PREV) {
122 if(highlighted_group > 0) {
123 if(highlighted_group == first_group) first_group--;
124 highlighted_group--;
125 }
126 draw_groups(db, first_group, highlighted_group, TRUE);
127 draw_entries(db, highlighted_group, first_entry, highlighted_entry, FALSE, FALSE);
128 } else if(b->command == C_NEXT) {
129 if(highlighted_group < db->groups_len - 1) {
130 getmaxyx(_groups_win, max_y, max_x);
131 if(highlighted_group == first_group + max_y - 1) first_group++;
132
133 highlighted_group++;
134 }
135 draw_groups(db, first_group, highlighted_group, TRUE);
136 draw_entries(db, highlighted_group, first_entry, highlighted_entry, FALSE, FALSE);
137 } else if(b->command == C_SELECT) {
138 selected_group = highlighted_group;
139 first_entry = 0;
140 highlighted_entry = 0;
141 entries_in_selected_group = entries_in_group(db, selected_group);
142 active_win = WIN_ENTRIES;
143 draw_groups(db, first_group, highlighted_group, FALSE);
144 draw_entries(db, selected_group, first_entry, highlighted_entry, TRUE, FALSE);
145 draw_bottom_bar(active_win);
146 } else if(b->command == C_OPEN) {
147 if(!open_database_form(new_db_filename, MAX_FILENAME_LENGTH)) {
148 new_db = malloc(sizeof(kpass_db));
149
150 result = open_db(new_db, new_db_filename);
151 if(!result) {
152 kpass_free_db(db);
153 free(db);
154 db = new_db;
155 strncpy(db_filename, new_db_filename, MAX_FILENAME_LENGTH);
156
157 first_group = 0;
158 selected_group = 0;
159 highlighted_group = 0;
160 first_entry = 0;
161 highlighted_entry = 0;
162 } else if(result < 0) {
163 kpass_free_db(new_db);
164 free(new_db);
165 } else {
166 free(new_db);
167 }
168 }
169
170 init_windows(db);
171 draw_groups(db, first_group, highlighted_group, TRUE);
172 draw_entries(db, highlighted_group, first_entry, highlighted_entry, FALSE, FALSE);
173 draw_top_bar(db_filename);
174 draw_bottom_bar(active_win);
175 } else if(b->command == C_QUIT) {
176 goto quit;
177 }
178 } else if(active_win == WIN_ENTRIES) {
179 for(b = _entries_bindings; b->key && b->key != press; b++);
180 if(!b->key) continue;
181
182 if(b->command == C_REVEAL) {
183 reveal = !reveal;
184 draw_entries(db, selected_group, first_entry, highlighted_entry, TRUE, reveal);
185 continue;
186 } else {
187 reveal = FALSE;
188 }
189
190 if(b->command == C_PREV) {
191 if(highlighted_entry > 0) {
192 if(highlighted_entry == first_entry) first_entry--;
193 highlighted_entry--;
194 }
195 draw_entries(db, selected_group, first_entry, highlighted_entry, TRUE, FALSE);
196 reveal = FALSE;
197 } else if(b->command == C_NEXT) {
198 if(highlighted_entry < entries_in_selected_group - 1) {
199 getmaxyx(_entries_win, max_y, max_x);
200 if(highlighted_entry == first_entry + max_y - 1) first_entry++;
201
202 highlighted_entry++;
203 }
204 draw_entries(db, selected_group, first_entry, highlighted_entry, TRUE, FALSE);
205 reveal = FALSE;
206 } else if(b->command == C_GROUPS) {
207 active_win = WIN_GROUPS;
208 first_entry = 0;
209 highlighted_entry = 0;
210 draw_entries(db, selected_group, first_entry, highlighted_entry, FALSE, FALSE);
211 draw_groups(db, first_group, highlighted_group, TRUE);
212 draw_bottom_bar(active_win);
213 } else if(b->command == C_EDIT) {
214 entry_form(db, nth_entry_in_group(db, selected_group, highlighted_entry));
215
216 /* This redraw needs to be revisited. */
217 draw_entries(db, selected_group, first_entry, highlighted_entry, TRUE, FALSE);
218 draw_groups(db, first_group, highlighted_group, FALSE);
219 } else if(b->command == C_XCLIP) {
220 entry = nth_entry_in_group(db, selected_group, highlighted_entry);
221 pipeout("/usr/bin/xclip", entry->password);
222 } else if(b->command == C_OPEN) {
223 if(!open_database_form(new_db_filename, MAX_FILENAME_LENGTH)) {
224 new_db = malloc(sizeof(kpass_db));
225
226 result = open_db(new_db, new_db_filename);
227 if(!result) {
228 kpass_free_db(db);
229 free(db);
230 db = new_db;
231 strncpy(db_filename, new_db_filename, MAX_FILENAME_LENGTH);
232
233 active_win = WIN_GROUPS;
234 first_group = 0;
235 selected_group = 0;
236 highlighted_group = 0;
237 first_entry = 0;
238 highlighted_entry = 0;
239 } else if(result < 0) {
240 kpass_free_db(new_db);
241 free(new_db);
242 } else {
243 free(new_db);
244 }
245 }
246
247 init_windows(db);
248
249 if(new_db == db) {
250 draw_entries(db, selected_group, first_entry, highlighted_entry, FALSE, FALSE);
251 draw_groups(db, first_group, highlighted_group, TRUE);
252 } else {
253 draw_entries(db, selected_group, first_entry, highlighted_entry, TRUE, FALSE);
254 draw_groups(db, first_group, highlighted_group, FALSE);
255 }
256
257 draw_top_bar(db_filename);
258 draw_bottom_bar(active_win);
259 } else if(b->command == C_QUIT) {
260 goto quit;
261 }
262 }
263 }
264
265 quit:
266
267 endwin();
268 free(_groups_bindings);
269 free(_entries_bindings);
270
271 kpass_free_db(db);
272 free(db);
273 return 0;
274 }
275
draw_top_bar(const char * s)276 void draw_top_bar(const char *s)
277 {
278 int max_x, max_y;
279
280 wmove(_top_bar, 0, 0);
281 waddstr(_top_bar, s);
282
283 getmaxyx(_top_bar, max_y, max_x);
284 whline(_top_bar, ' ', max_x);
285 wrefresh(_top_bar);
286 }
287
draw_bottom_bar(int mode)288 void draw_bottom_bar(int mode)
289 {
290 int max_x, max_y;
291 struct binding *b;
292
293 wmove(_bottom_bar, 0, 0);
294
295 if(mode == WIN_GROUPS || mode == WIN_ENTRIES) {
296 b = mode == WIN_GROUPS ? _groups_bindings : _entries_bindings;
297
298 for(; b->key; b++) {
299 switch(b->key) {
300 case '\t': waddstr(_bottom_bar, "[tab]"); break;
301 case '\n': waddstr(_bottom_bar, "[return]"); break;
302 case ' ': waddstr(_bottom_bar, "[space]"); break;
303 case KEY_UP: continue;
304 case KEY_DOWN: continue;
305 default: waddch(_bottom_bar, b->key); break;
306 }
307
308 if((b+1)->key && (b+1)->command == b->command) {
309 waddch(_bottom_bar, ',');
310 continue;
311 }
312
313 waddch(_bottom_bar, ':');
314 waddstr(_bottom_bar, b->command);
315 waddstr(_bottom_bar, " ");
316 }
317 }
318
319 getmaxyx(_bottom_bar, max_y, max_x);
320 whline(_bottom_bar, ' ', max_x);
321 wrefresh(_bottom_bar);
322 }
323
fatal(const char * message)324 void fatal(const char *message)
325 {
326 endwin();
327
328 fprintf(stderr, "%s\n", message);
329 exit(1);
330 }
331
open_db(kpass_db * db,const char * filename)332 int open_db(kpass_db *db, const char *filename)
333 {
334 char password[MAX_PASSWORD_LENGTH];
335 bool decrypt_success = FALSE;
336 int retval = 0;
337
338 clear();
339 refresh();
340
341 if(read_db(db, filename)) {
342 error_dialog("Couldn't read.");
343 clear();
344 refresh();
345 return 1;
346 }
347
348 while(!decrypt_success) {
349 if(password_form(password, MAX_PASSWORD_LENGTH)) {
350 retval = -1;
351 clear();
352 refresh();
353 break;
354 }
355
356 if(decrypt_db(db, password)) {
357 error_dialog("Password does not match.");
358 } else {
359 decrypt_success = TRUE;
360 }
361
362 clear();
363 refresh();
364 }
365
366 // Clear password since we don't need it anymore.
367 memset(password, 0, MAX_PASSWORD_LENGTH);
368
369 return retval;
370 }
371
read_db(kpass_db * db,const char * filename)372 int read_db(kpass_db *db, const char *filename)
373 {
374 uint8_t *data;
375 kpass_retval retval;
376 struct stat sb;
377 int fd;
378
379 fd = open(filename, O_RDONLY);
380 if(fd < 0) return errno;
381
382 if(fstat(fd, &sb) < 0) return errno;
383
384 data = mmap(NULL, sb.st_size, PROT_READ, MAP_SHARED, fd, 0);
385 if(data == MAP_FAILED) return errno;
386
387 retval = kpass_init_db(db, data, sb.st_size);
388 if(retval) return -1;
389 munmap(data, sb.st_size);
390
391 return 0;
392 }
393
decrypt_db(kpass_db * db,const char * password)394 int decrypt_db(kpass_db *db, const char *password)
395 {
396 kpass_retval retval;
397 unsigned char pw_hash[32];
398
399 kpass_hash_pw(password, pw_hash);
400
401 retval = kpass_decrypt_db(db, pw_hash);
402 if(retval) return -1;
403
404 return 0;
405 }
406
draw_groups(const kpass_db * db,int first,int highlighted,bool foreground)407 void draw_groups(const kpass_db *db, int first, int highlighted, bool foreground)
408 {
409 int max_x, max_y;
410 int i, j;
411 int y;
412 int highlight_attrib;
413
414 getmaxyx(_groups_win, max_y, max_x);
415 highlight_attrib = foreground ? A_REVERSE : A_NORMAL;
416
417 for(i = first, y = 0; i < db->groups_len; i++, y++) {
418 wattrset(_groups_win, i == highlighted ? highlight_attrib : A_NORMAL);
419
420 if(db->groups[i]->level > 0) {
421 if(y < max_y) {
422 wmove(_groups_win, y, 0);
423 whline(_groups_win, ' ', 3*(db->groups[i]->level - 1));
424 wmove(_groups_win, y, 3*(db->groups[i]->level - 1));
425 waddch(_groups_win, ACS_LLCORNER);
426 waddch(_groups_win, '-');
427 waddch(_groups_win, '>');
428 }
429
430 for(j = i-1; j >= 0 && j >= first && db->groups[j]->level >= db->groups[i]->level; j--) {
431 if(y - (i-j) >= max_y) continue;
432
433 wmove(_groups_win, y - (i-j), 3*(db->groups[i]->level - 1));
434
435 wattrset(_groups_win, j == highlighted ? highlight_attrib : A_NORMAL);
436
437 if(db->groups[j]->level == db->groups[i]->level) {
438 waddch(_groups_win, ACS_LTEE);
439 } else {
440 waddch(_groups_win, ACS_VLINE);
441 }
442 }
443
444 if(y < max_y) {
445 wattrset(_groups_win, i == highlighted ? highlight_attrib : A_NORMAL);
446 wmove(_groups_win, y, 3*(db->groups[i]->level));
447 }
448 } else {
449 if(y < max_y) wmove(_groups_win, y, 0);
450 }
451
452 if(y < max_y) {
453 waddstr(_groups_win, db->groups[i]->name);
454
455 whline(_groups_win, ' ', max_x);
456 if(i == highlighted) mvwaddch(_groups_win, y, max_x - 2, '*');
457 }
458 }
459
460 wattrset(_groups_win, A_NORMAL);
461
462 /* Blank the rest of the window. */
463 for(; y < max_y; y++) {
464 mvwhline(_groups_win, y, 0, ' ', max_x);
465 }
466
467 wrefresh(_groups_win);
468 }
469
draw_entries(const kpass_db * db,int group,int first,int highlighted,bool foreground,bool reveal)470 void draw_entries(const kpass_db *db, int group, int first, int highlighted, bool foreground, bool reveal)
471 {
472 int max_x;
473 int max_y;
474 int i;
475 int c; /* Accumulator for count of entries in group. */
476 int x, y;
477
478 getmaxyx(_entries_win, max_y, max_x);
479
480 for(i = 0, y = 0, c = 0; i < db->entries_len && y < max_y; i++) {
481 if(db->entries[i]->group_id != db->groups[group]->id) continue;
482 if(c < first) {
483 /* Increase count since we did encounter one, we just aren't showing it. */
484 c++;
485 continue;
486 }
487
488 if(foreground) wattrset(_entries_win, c == highlighted ? A_REVERSE : A_NORMAL);
489
490 mvwhline(_entries_win, y, 0, ' ', max_x); /* clear line first */
491
492 x = 0;
493 mvwaddnstr(_entries_win, y, x, db->entries[i]->title, _entry_widths[0] - 1);
494 if(strlen(db->entries[i]->title) > _entry_widths[0] - 1 ||
495 x + MIN(strlen(db->entries[i]->title), _entry_widths[0]) >= max_x)
496 mvwaddch(_entries_win,
497 y, MIN(x + _entry_widths[0] - 2, max_x - 1),
498 '+' | A_REVERSE);
499
500 x += _entry_widths[0];
501 mvwaddnstr(_entries_win, y, x, db->entries[i]->username, _entry_widths[1] - 1);
502 if(strlen(db->entries[i]->username) > _entry_widths[1] - 1 ||
503 x + MIN(strlen(db->entries[i]->username), _entry_widths[1]) >= max_x)
504 mvwaddch(_entries_win,
505 y, MIN(x + _entry_widths[1] - 2, max_x - 1),
506 '+' | A_REVERSE);
507
508 x += _entry_widths[1];
509 if(reveal && c == highlighted) {
510 mvwaddnstr(_entries_win, y, x, db->entries[i]->password, _entry_widths[2] - 1);
511 if(strlen(db->entries[i]->password) > _entry_widths[2] - 1 ||
512 x + MIN(strlen(db->entries[i]->password), _entry_widths[2]) >= max_x)
513 mvwaddch(_entries_win,
514 y, MIN(x + _entry_widths[2] - 2, max_x - 1),
515 '+' | A_REVERSE);
516 } else {
517 mvwaddstr(_entries_win, y, x, "********");
518 }
519
520 x += _entry_widths[2];
521 mvwaddnstr(_entries_win, y, x, db->entries[i]->url, _entry_widths[3]);
522 if(strlen(db->entries[i]->url) > _entry_widths[3] ||
523 x + MIN(strlen(db->entries[i]->url), _entry_widths[3]) >= max_x)
524 mvwaddch(_entries_win,
525 y, MIN(x + _entry_widths[3] - 1, max_x - 1),
526 '+' | A_REVERSE);
527
528 y++;
529 c++;
530 }
531
532 wattrset(_entries_win, A_NORMAL);
533
534 /* Blank the rest of the window. */
535 for(; y < max_y; y++) {
536 mvwhline(_entries_win, y, 0, ' ', max_x);
537 }
538
539 wrefresh(_entries_win);
540 }
541
entries_in_group(const kpass_db * db,int group)542 int entries_in_group(const kpass_db *db, int group)
543 {
544 int i;
545 int n = 0;
546
547 for(i=0; i < db->entries_len; i++) {
548 if(db->entries[i]->group_id == db->groups[group]->id) n++;
549 }
550
551 return n;
552 }
553
nth_entry_in_group(const kpass_db * db,int group,int n)554 struct kpass_entry *nth_entry_in_group(const kpass_db *db, int group, int n)
555 {
556 int i;
557
558 /* Add 1 since n is expected to start at 0
559 * (first listed entry for the group is called 0). */
560 n++;
561
562 for(i=0; i < db->entries_len; i++) {
563 if(db->entries[i]->group_id == db->groups[group]->id) n--;
564 if(!n) return db->entries[i];
565 }
566
567 return 0;
568 }
569
find_groups_win_width(const kpass_db * db)570 int find_groups_win_width(const kpass_db *db)
571 {
572 int i;
573 int n = 0;
574
575 for(i=0; i < db->groups_len; i++) {
576 if(strlen(db->groups[i]->name) + 3*(db->groups[i]->level) > n)
577 n = strlen(db->groups[i]->name) + 3*(db->groups[i]->level);
578 }
579
580 /* Longest line plus borders plus " * ". */
581 return n + 5;
582 }
583
init_windows(const kpass_db * db)584 void init_windows(const kpass_db *db)
585 {
586 int max_y, max_x;
587 int x;
588 int groups_win_width;
589
590 if(_groups_super_win) delwin(_groups_super_win);
591 if(_groups_win) delwin(_groups_super_win);
592 if(_entries_super_win) delwin(_entries_super_win);
593 if(_entries_win) delwin(_entries_super_win);
594 if(_top_bar) delwin(_top_bar);
595 if(_bottom_bar) delwin(_bottom_bar);
596
597 getmaxyx(stdscr, max_y, max_x);
598 groups_win_width = find_groups_win_width(db);
599
600 _groups_super_win = newwin(max_y - 2, groups_win_width, 1, 0);
601 _groups_win = derwin(_groups_super_win, max_y - 6, groups_win_width - 2, 3, 1);
602 _entries_super_win = newwin(max_y - 2, max_x - groups_win_width, 1, groups_win_width);
603 _entries_win = derwin(_entries_super_win, max_y - 6, max_x - groups_win_width - 2, 3, 1);
604 _top_bar = newwin(1, max_x, 0, 0);
605 _bottom_bar = newwin(1, max_x, max_y - 1, 0);
606
607 keypad(_groups_win, TRUE);
608 box(_groups_super_win, 0, 0);
609 mvwaddch(_groups_super_win, 2, 0, ACS_LTEE);
610 mvwhline(_groups_super_win, 2, 1, ACS_HLINE, groups_win_width - 2);
611 mvwaddch(_groups_super_win, 2, groups_win_width - 1, ACS_RTEE);
612 mvwaddstr(_groups_super_win, 1, 1, "Groups");
613
614 keypad(_entries_win, TRUE);
615 box(_entries_super_win, 0, 0);
616 mvwaddch(_entries_super_win, 2, 0, ACS_LTEE);
617 mvwhline(_entries_super_win, 2, 1, ACS_HLINE, max_x - groups_win_width - 2);
618 mvwaddch(_entries_super_win, 2, max_x - groups_win_width - 1, ACS_RTEE);
619
620 x = 1;
621 mvwaddstr(_entries_super_win, 1, 1, "Title");
622 x += _entry_widths[0];
623 mvwaddstr(_entries_super_win, 1, x, "Username");
624 x += _entry_widths[1];
625 mvwaddstr(_entries_super_win, 1, x, "Password");
626 x += _entry_widths[2];
627 mvwaddstr(_entries_super_win, 1, x, "URL");
628
629 wrefresh(_groups_super_win);
630 wrefresh(_entries_super_win);
631 }
632
error_dialog(char * s)633 void error_dialog(char *s) {
634 WINDOW *win;
635 int press;
636 int max_x, max_y;
637 int rows, cols;
638
639 getmaxyx(stdscr, max_y, max_x);
640
641 rows = 7;
642 cols = strlen(s) + 4;
643 win = newwin(rows, cols, (max_y - rows) / 2, (max_x - cols) / 2);
644 keypad(win, TRUE);
645 box(win, 0, 0);
646
647 mvwaddstr(win, 2, 2, s);
648 wattrset(win, A_REVERSE);
649 mvwaddstr(win, 4, cols / 2 - 2, "[OK]");
650 wattrset(win, A_NORMAL);
651
652 wrefresh(win);
653
654 while(wgetch(win) != '\n');
655
656 delwin(win);
657 }
658
pipeout(const char * command,const char * s)659 int pipeout(const char *command, const char *s)
660 {
661 int pipefd[2];
662 pid_t pid;
663
664 if(pipe(pipefd) < 0) return -1;
665
666 pid = fork();
667 if(pid < 0) return -1;
668
669 if(pid) {
670 /* parent */
671 close(pipefd[0]); /* Close read end. */
672 write(pipefd[1], s, strlen(s));
673 close(pipefd[1]);
674 wait(0); /* Wait for child. */
675 } else {
676 /* child */
677 close(pipefd[1]); /* Close write end. */
678 dup2(pipefd[0], 0); /* Dup read end to stdin. */
679
680 execl(command, command, 0);
681
682 /* I don't really think these are necessary, but just in case. */
683 close(pipefd[0]);
684 exit(0);
685 }
686
687 return 0;
688 }
689
init_bindings()690 void init_bindings()
691 {
692 _groups_bindings = new_binding_set();
693 add_binding(&_groups_bindings, 'o', C_OPEN);
694 add_binding(&_groups_bindings, 's', C_SAVE);
695 add_binding(&_groups_bindings, 'a', C_ADD_GROUP);
696 add_binding(&_groups_bindings, KEY_UP, C_PREV);
697 add_binding(&_groups_bindings, KEY_DOWN, C_NEXT);
698 add_binding(&_groups_bindings, '\t', C_SELECT);
699 add_binding(&_groups_bindings, '\n', C_SELECT);
700 add_binding(&_groups_bindings, 'e', C_EDIT);
701 add_binding(&_groups_bindings, 'q', C_QUIT);
702
703 _entries_bindings = new_binding_set();
704 add_binding(&_entries_bindings, 'o', C_OPEN);
705 add_binding(&_entries_bindings, 's', C_SAVE);
706 add_binding(&_entries_bindings, 'a', C_ADD_ENTRY);
707 add_binding(&_entries_bindings, '\t', C_GROUPS);
708 add_binding(&_entries_bindings, KEY_UP, C_PREV);
709 add_binding(&_entries_bindings, KEY_DOWN, C_NEXT);
710 add_binding(&_entries_bindings, ' ', C_REVEAL);
711 add_binding(&_entries_bindings, 'r', C_REVEAL);
712 add_binding(&_entries_bindings, 'e', C_EDIT);
713 add_binding(&_entries_bindings, '\n', C_EDIT);
714 add_binding(&_entries_bindings, 'q', C_QUIT);
715
716 /* This should be optional. */
717 add_binding(&_entries_bindings, 'x', C_XCLIP);
718 }
719
720