+/*
+ * Keys
+ */
+
+struct keybinding {
+ int alias;
+ enum request request;
+ struct keybinding *next;
+};
+
+static struct keybinding default_keybindings[] = {
+ /* View switching */
+ { 'm', REQ_VIEW_MAIN },
+ { 'd', REQ_VIEW_DIFF },
+ { 'l', REQ_VIEW_LOG },
+ { 'p', REQ_VIEW_PAGER },
+ { 'h', REQ_VIEW_HELP },
+ { '?', REQ_VIEW_HELP },
+
+ /* View manipulation */
+ { 'q', REQ_VIEW_CLOSE },
+ { KEY_TAB, REQ_VIEW_NEXT },
+ { KEY_RETURN, REQ_ENTER },
+ { KEY_UP, REQ_PREVIOUS },
+ { KEY_DOWN, REQ_NEXT },
+
+ /* Cursor navigation */
+ { 'k', REQ_MOVE_UP },
+ { 'j', REQ_MOVE_DOWN },
+ { KEY_HOME, REQ_MOVE_FIRST_LINE },
+ { KEY_END, REQ_MOVE_LAST_LINE },
+ { KEY_NPAGE, REQ_MOVE_PAGE_DOWN },
+ { ' ', REQ_MOVE_PAGE_DOWN },
+ { KEY_PPAGE, REQ_MOVE_PAGE_UP },
+ { 'b', REQ_MOVE_PAGE_UP },
+ { '-', REQ_MOVE_PAGE_UP },
+
+ /* Scrolling */
+ { KEY_IC, REQ_SCROLL_LINE_UP },
+ { KEY_DC, REQ_SCROLL_LINE_DOWN },
+ { 'w', REQ_SCROLL_PAGE_UP },
+ { 's', REQ_SCROLL_PAGE_DOWN },
+
+ /* Misc */
+ { 'Q', REQ_QUIT },
+ { 'z', REQ_STOP_LOADING },
+ { 'v', REQ_SHOW_VERSION },
+ { 'r', REQ_SCREEN_REDRAW },
+ { 'n', REQ_TOGGLE_LINENO },
+ { 'g', REQ_TOGGLE_REV_GRAPH},
+ { ':', REQ_PROMPT },
+
+ /* wgetch() with nodelay() enabled returns ERR when there's no input. */
+ { ERR, REQ_SCREEN_UPDATE },
+
+ /* Use the ncurses SIGWINCH handler. */
+ { KEY_RESIZE, REQ_SCREEN_RESIZE },
+};
+
+#define KEYMAP_INFO \
+ KEYMAP_(GENERIC), \
+ KEYMAP_(MAIN), \
+ KEYMAP_(DIFF), \
+ KEYMAP_(LOG), \
+ KEYMAP_(PAGER), \
+ KEYMAP_(HELP) \
+
+enum keymap {
+#define KEYMAP_(name) KEYMAP_##name
+ KEYMAP_INFO
+#undef KEYMAP_
+};
+
+static struct int_map keymap_table[] = {
+#define KEYMAP_(name) { #name, STRING_SIZE(#name), KEYMAP_##name }
+ KEYMAP_INFO
+#undef KEYMAP_
+};
+
+#define set_keymap(map, name) \
+ set_from_int_map(keymap_table, ARRAY_SIZE(keymap_table), map, name, strlen(name))
+
+static struct keybinding *keybindings[ARRAY_SIZE(keymap_table)];
+
+static void
+add_keybinding(enum keymap keymap, enum request request, int key)
+{
+ struct keybinding *keybinding;
+
+ keybinding = calloc(1, sizeof(*keybinding));
+ if (!keybinding)
+ die("Failed to allocate keybinding");
+
+ keybinding->alias = key;
+ keybinding->request = request;
+ keybinding->next = keybindings[keymap];
+ keybindings[keymap] = keybinding;
+}
+
+/* Looks for a key binding first in the given map, then in the generic map, and
+ * lastly in the default keybindings. */
+static enum request
+get_keybinding(enum keymap keymap, int key)
+{
+ struct keybinding *kbd;
+ int i;
+
+ for (kbd = keybindings[keymap]; kbd; kbd = kbd->next)
+ if (kbd->alias == key)
+ return kbd->request;
+
+ for (kbd = keybindings[KEYMAP_GENERIC]; kbd; kbd = kbd->next)
+ if (kbd->alias == key)
+ return kbd->request;
+
+ for (i = 0; i < ARRAY_SIZE(default_keybindings); i++)
+ if (default_keybindings[i].alias == key)
+ return default_keybindings[i].request;
+
+ return (enum request) key;
+}
+
+
+struct key {
+ char *name;
+ int value;
+};
+
+static struct key key_table[] = {
+ { "Enter", KEY_RETURN },
+ { "Space", ' ' },
+ { "Backspace", KEY_BACKSPACE },
+ { "Tab", KEY_TAB },
+ { "Escape", KEY_ESC },
+ { "Left", KEY_LEFT },
+ { "Right", KEY_RIGHT },
+ { "Up", KEY_UP },
+ { "Down", KEY_DOWN },
+ { "Insert", KEY_IC },
+ { "Delete", KEY_DC },
+ { "Hash", '#' },
+ { "Home", KEY_HOME },
+ { "End", KEY_END },
+ { "PageUp", KEY_PPAGE },
+ { "PageDown", KEY_NPAGE },
+ { "F1", KEY_F(1) },
+ { "F2", KEY_F(2) },
+ { "F3", KEY_F(3) },
+ { "F4", KEY_F(4) },
+ { "F5", KEY_F(5) },
+ { "F6", KEY_F(6) },
+ { "F7", KEY_F(7) },
+ { "F8", KEY_F(8) },
+ { "F9", KEY_F(9) },
+ { "F10", KEY_F(10) },
+ { "F11", KEY_F(11) },
+ { "F12", KEY_F(12) },
+};
+
+static int
+get_key_value(const char *name)
+{
+ int i;
+
+ for (i = 0; i < ARRAY_SIZE(key_table); i++)
+ if (!strcasecmp(key_table[i].name, name))
+ return key_table[i].value;
+
+ if (strlen(name) == 1 && isprint(*name))
+ return (int) *name;
+
+ return ERR;
+}
+
+static char *
+get_key(enum request request)
+{
+ static char buf[BUFSIZ];
+ static char key_char[] = "'X'";
+ int pos = 0;
+ char *sep = " ";
+ int i;
+
+ buf[pos] = 0;
+
+ for (i = 0; i < ARRAY_SIZE(default_keybindings); i++) {
+ struct keybinding *keybinding = &default_keybindings[i];
+ char *seq = NULL;
+ int key;
+
+ if (keybinding->request != request)
+ continue;
+
+ for (key = 0; key < ARRAY_SIZE(key_table); key++)
+ if (key_table[key].value == keybinding->alias)
+ seq = key_table[key].name;
+
+ if (seq == NULL &&
+ keybinding->alias < 127 &&
+ isprint(keybinding->alias)) {
+ key_char[1] = (char) keybinding->alias;
+ seq = key_char;
+ }
+
+ if (!seq)
+ seq = "'?'";
+
+ if (!string_format_from(buf, &pos, "%s%s", sep, seq))
+ return "Too many keybindings!";
+ sep = ", ";
+ }
+
+ return buf;
+}
+
+
+/*
+ * User config file handling.
+ */
+
+static struct int_map color_map[] = {
+#define COLOR_MAP(name) { #name, STRING_SIZE(#name), COLOR_##name }
+ COLOR_MAP(DEFAULT),
+ COLOR_MAP(BLACK),
+ COLOR_MAP(BLUE),
+ COLOR_MAP(CYAN),
+ COLOR_MAP(GREEN),
+ COLOR_MAP(MAGENTA),
+ COLOR_MAP(RED),
+ COLOR_MAP(WHITE),
+ COLOR_MAP(YELLOW),
+};
+
+#define set_color(color, name) \
+ set_from_int_map(color_map, ARRAY_SIZE(color_map), color, name, strlen(name))
+
+static struct int_map attr_map[] = {
+#define ATTR_MAP(name) { #name, STRING_SIZE(#name), A_##name }
+ ATTR_MAP(NORMAL),
+ ATTR_MAP(BLINK),
+ ATTR_MAP(BOLD),
+ ATTR_MAP(DIM),
+ ATTR_MAP(REVERSE),
+ ATTR_MAP(STANDOUT),
+ ATTR_MAP(UNDERLINE),
+};
+
+#define set_attribute(attr, name) \
+ set_from_int_map(attr_map, ARRAY_SIZE(attr_map), attr, name, strlen(name))
+
+static int config_lineno;
+static bool config_errors;
+static char *config_msg;
+
+/* Wants: object fgcolor bgcolor [attr] */
+static int
+option_color_command(int argc, char *argv[])
+{
+ struct line_info *info;
+
+ if (argc != 3 && argc != 4) {
+ config_msg = "Wrong number of arguments given to color command";
+ return ERR;
+ }
+
+ info = get_line_info(argv[0], strlen(argv[0]));
+ if (!info) {
+ config_msg = "Unknown color name";
+ return ERR;
+ }
+
+ if (set_color(&info->fg, argv[1]) == ERR ||
+ set_color(&info->bg, argv[2]) == ERR) {
+ config_msg = "Unknown color";
+ return ERR;
+ }
+
+ if (argc == 4 && set_attribute(&info->attr, argv[3]) == ERR) {
+ config_msg = "Unknown attribute";
+ return ERR;
+ }
+
+ return OK;
+}
+
+/* Wants: name = value */
+static int
+option_set_command(int argc, char *argv[])
+{
+ if (argc != 3) {
+ config_msg = "Wrong number of arguments given to set command";
+ return ERR;
+ }
+
+ if (strcmp(argv[1], "=")) {
+ config_msg = "No value assigned";
+ return ERR;
+ }
+
+ if (!strcmp(argv[0], "show-rev-graph")) {
+ opt_rev_graph = (!strcmp(argv[2], "1") ||
+ !strcmp(argv[2], "true") ||
+ !strcmp(argv[2], "yes"));
+ return OK;
+ }
+
+ if (!strcmp(argv[0], "line-number-interval")) {
+ opt_num_interval = atoi(argv[2]);
+ return OK;
+ }
+
+ if (!strcmp(argv[0], "tab-size")) {
+ opt_tab_size = atoi(argv[2]);
+ return OK;
+ }
+
+ if (!strcmp(argv[0], "commit-encoding")) {
+ char *arg = argv[2];
+ int delimiter = *arg;
+ int i;
+
+ switch (delimiter) {
+ case '"':
+ case '\'':
+ for (arg++, i = 0; arg[i]; i++)
+ if (arg[i] == delimiter) {
+ arg[i] = 0;
+ break;
+ }
+ default:
+ string_copy(opt_encoding, arg);
+ return OK;
+ }
+ }
+
+ config_msg = "Unknown variable name";
+ return ERR;
+}
+
+/* Wants: mode request key */
+static int
+option_bind_command(int argc, char *argv[])
+{
+ enum request request;
+ int keymap;
+ int key;
+
+ if (argc != 3) {
+ config_msg = "Wrong number of arguments given to bind command";
+ return ERR;
+ }
+
+ if (set_keymap(&keymap, argv[0]) == ERR) {
+ config_msg = "Unknown key map";
+ return ERR;
+ }
+
+ key = get_key_value(argv[1]);
+ if (key == ERR) {
+ config_msg = "Unknown key";
+ return ERR;
+ }
+
+ request = get_request(argv[2]);
+ if (request == REQ_UNKNOWN) {
+ config_msg = "Unknown request name";
+ return ERR;
+ }
+
+ add_keybinding(keymap, request, key);
+
+ return OK;
+}
+
+static int
+set_option(char *opt, char *value)
+{
+ char *argv[16];
+ int valuelen;
+ int argc = 0;
+
+ /* Tokenize */
+ while (argc < ARRAY_SIZE(argv) && (valuelen = strcspn(value, " \t"))) {
+ argv[argc++] = value;
+
+ value += valuelen;
+ if (!*value)
+ break;
+
+ *value++ = 0;
+ while (isspace(*value))
+ value++;
+ }
+
+ if (!strcmp(opt, "color"))
+ return option_color_command(argc, argv);
+
+ if (!strcmp(opt, "set"))
+ return option_set_command(argc, argv);
+
+ if (!strcmp(opt, "bind"))
+ return option_bind_command(argc, argv);
+
+ config_msg = "Unknown option command";
+ return ERR;
+}
+
+static int
+read_option(char *opt, int optlen, char *value, int valuelen)
+{
+ int status = OK;
+
+ config_lineno++;
+ config_msg = "Internal error";
+
+ /* Check for comment markers, since read_properties() will
+ * only ensure opt and value are split at first " \t". */
+ optlen = strcspn(opt, "#");
+ if (optlen == 0)
+ return OK;
+
+ if (opt[optlen] != 0) {
+ config_msg = "No option value";
+ status = ERR;
+
+ } else {
+ /* Look for comment endings in the value. */
+ int len = strcspn(value, "#");
+
+ if (len < valuelen) {
+ valuelen = len;
+ value[valuelen] = 0;
+ }
+
+ status = set_option(opt, value);
+ }
+
+ if (status == ERR) {
+ fprintf(stderr, "Error on line %d, near '%.*s': %s\n",
+ config_lineno, optlen, opt, config_msg);
+ config_errors = TRUE;
+ }
+
+ /* Always keep going if errors are encountered. */
+ return OK;
+}
+
+static int
+load_options(void)
+{
+ char *home = getenv("HOME");
+ char buf[1024];
+ FILE *file;
+
+ config_lineno = 0;
+ config_errors = FALSE;
+
+ if (!home || !string_format(buf, "%s/.tigrc", home))
+ return ERR;
+
+ /* It's ok that the file doesn't exist. */
+ file = fopen(buf, "r");
+ if (!file)
+ return OK;
+
+ if (read_properties(file, " \t", read_option) == ERR ||
+ config_errors == TRUE)
+ fprintf(stderr, "Errors while loading %s.\n", buf);
+
+ return OK;
+}
+
+
+/*