1 /* Copyright (c) 2006-2014 Jonas Fonseca <jonas.fonseca@gmail.com>
3 * This program is free software; you can redistribute it and/or
4 * modify it under the terms of the GNU General Public License as
5 * published by the Free Software Foundation; either version 2 of
6 * the License, or (at your option) any later version.
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
14 #define WARN_MISSING_CURSES_CONFIGURATION
35 forward_request_to_child(struct view
*child
, enum request request
)
37 return displayed_views() == 2 && view_is_displayed(child
) &&
38 !strcmp(child
->vid
, child
->ops
->id
);
42 view_request(struct view
*view
, enum request request
)
44 if (!view
|| !view
->lines
)
47 if (request
== REQ_ENTER
&& !opt_focus_child
&&
48 view_has_flags(view
, VIEW_SEND_CHILD_ENTER
)) {
49 struct view
*child
= display
[1];
51 if (forward_request_to_child(child
, request
)) {
52 view_request(child
, request
);
57 if (request
== REQ_REFRESH
&& view
->unrefreshable
) {
58 report("This view can not be refreshed");
62 return view
->ops
->request(view
, request
, &view
->line
[view
->pos
.lineno
]);
69 #define VIEW_FLAG_RESET_DISPLAY ((enum view_flag) -1)
71 #define TOGGLE_MENU_INFO(_) \
72 _(LINENO, '.', "line numbers", &opt_show_line_numbers, NULL, VIEW_NO_FLAGS), \
73 _(DATE, 'D', "dates", &opt_show_date, date_map, VIEW_NO_FLAGS), \
74 _(AUTHOR, 'A', "author", &opt_show_author, author_map, VIEW_NO_FLAGS), \
75 _(GRAPHIC, '~', "graphics", &opt_line_graphics, graphic_map, VIEW_NO_FLAGS), \
76 _(REV_GRAPH, 'g', "revision graph", &opt_show_rev_graph, NULL, VIEW_LOG_LIKE), \
77 _(FILENAME, '#', "file names", &opt_show_filename, filename_map, VIEW_NO_FLAGS), \
78 _(FILE_SIZE, '*', "file sizes", &opt_show_file_size, file_size_map, VIEW_NO_FLAGS), \
79 _(IGNORE_SPACE, 'W', "space changes", &opt_ignore_space, ignore_space_map, VIEW_DIFF_LIKE), \
80 _(COMMIT_ORDER, 'l', "commit order", &opt_commit_order, commit_order_map, VIEW_LOG_LIKE), \
81 _(REFS, 'F', "reference display", &opt_show_refs, NULL, VIEW_NO_FLAGS), \
82 _(CHANGES, 'C', "local change display", &opt_show_changes, NULL, VIEW_NO_FLAGS), \
83 _(ID, 'X', "commit ID display", &opt_show_id, NULL, VIEW_NO_FLAGS), \
84 _(FILES, '%', "file filtering", &opt_file_filter, NULL, VIEW_DIFF_LIKE | VIEW_LOG_LIKE), \
85 _(TITLE_OVERFLOW, '$', "commit title overflow display", &opt_title_overflow, NULL, VIEW_NO_FLAGS), \
86 _(UNTRACKED_DIRS, 'd', "untracked directory info", &opt_status_untracked_dirs, NULL, VIEW_STATUS_LIKE), \
87 _(VERTICAL_SPLIT, '|', "view split", &opt_vertical_split, vertical_split_map, VIEW_FLAG_RESET_DISPLAY), \
90 toggle_option(struct view
*view
, enum request request
, char msg
[SIZEOF_STR
])
94 const struct enum_map
*map
;
95 enum view_flag reload_flags
;
97 #define DEFINE_TOGGLE_DATA(id, key, help, value, map, vflags) { REQ_TOGGLE_ ## id, map, vflags }
98 TOGGLE_MENU_INFO(DEFINE_TOGGLE_DATA
)
100 const struct menu_item menu
[] = {
101 #define DEFINE_TOGGLE_MENU(id, key, help, value, map, vflags) { key, help, value }
102 TOGGLE_MENU_INFO(DEFINE_TOGGLE_MENU
)
107 if (request
== REQ_OPTIONS
) {
108 if (!prompt_menu("Toggle option", menu
, &i
))
109 return VIEW_NO_FLAGS
;
111 while (i
< ARRAY_SIZE(data
) && data
[i
].request
!= request
)
113 if (i
>= ARRAY_SIZE(data
))
114 die("Invalid request (%d)", request
);
117 if (data
[i
].map
!= NULL
) {
118 unsigned int *opt
= menu
[i
].data
;
120 *opt
= (*opt
+ 1) % data
[i
].map
->size
;
121 if (data
[i
].map
== ignore_space_map
) {
122 update_ignore_space_arg();
123 string_format_size(msg
, SIZEOF_STR
,
124 "Ignoring %s %s", enum_name(data
[i
].map
->entries
[*opt
]), menu
[i
].text
);
126 } else if (data
[i
].map
== commit_order_map
) {
127 update_commit_order_arg();
128 string_format_size(msg
, SIZEOF_STR
,
129 "Using %s %s", enum_name(data
[i
].map
->entries
[*opt
]), menu
[i
].text
);
132 string_format_size(msg
, SIZEOF_STR
,
133 "Displaying %s %s", enum_name(data
[i
].map
->entries
[*opt
]), menu
[i
].text
);
136 } else if (menu
[i
].data
== &opt_title_overflow
) {
137 int *option
= menu
[i
].data
;
139 *option
= *option
? -*option
: 50;
140 string_format_size(msg
, SIZEOF_STR
,
141 "%sabling %s", *option
> 0 ? "En" : "Dis", menu
[i
].text
);
144 bool *option
= menu
[i
].data
;
147 string_format_size(msg
, SIZEOF_STR
,
148 "%sabling %s", *option
? "En" : "Dis", menu
[i
].text
);
151 return data
[i
].reload_flags
;
159 static enum request
run_prompt_command(struct view
*view
, char *cmd
);
162 open_run_request(struct view
*view
, enum request request
)
164 struct run_request
*req
= get_run_request(request
);
165 const char **argv
= NULL
;
166 bool confirmed
= FALSE
;
171 report("Unknown run request");
175 if (format_argv(view
->env
, &argv
, req
->argv
, FALSE
, TRUE
)) {
177 char cmd
[SIZEOF_STR
];
179 if (argv_to_string(argv
, cmd
, sizeof(cmd
), " ")) {
180 request
= run_prompt_command(view
, cmd
);
184 confirmed
= !req
->confirm
;
187 char cmd
[SIZEOF_STR
], prompt
[SIZEOF_STR
];
188 const char *and_exit
= req
->exit
? " and exit" : "";
190 if (argv_to_string(argv
, cmd
, sizeof(cmd
), " ") &&
191 string_format(prompt
, "Run `%s`%s?", cmd
, and_exit
) &&
192 prompt_yesno(prompt
)) {
197 if (confirmed
&& argv_remove_quotes(argv
)) {
201 open_external_viewer(argv
, NULL
, !req
->exit
, "");
210 if (request
== REQ_NONE
) {
211 if (req
->confirm
&& !confirmed
)
217 else if (view_has_flags(view
, VIEW_REFRESH
) && !view
->unrefreshable
)
218 request
= REQ_REFRESH
;
224 * User request switch noodle
228 view_driver(struct view
*view
, enum request request
)
232 if (request
== REQ_NONE
)
235 if (request
>= REQ_RUN_REQUESTS
) {
236 request
= open_run_request(view
, request
);
238 // exit quickly rather than going through view_request and back
239 if (request
== REQ_QUIT
)
243 request
= view_request(view
, request
);
244 if (request
== REQ_NONE
)
250 case REQ_MOVE_PAGE_UP
:
251 case REQ_MOVE_PAGE_DOWN
:
252 case REQ_MOVE_FIRST_LINE
:
253 case REQ_MOVE_LAST_LINE
:
254 move_view(view
, request
);
257 case REQ_SCROLL_FIRST_COL
:
258 case REQ_SCROLL_LEFT
:
259 case REQ_SCROLL_RIGHT
:
260 case REQ_SCROLL_LINE_DOWN
:
261 case REQ_SCROLL_LINE_UP
:
262 case REQ_SCROLL_PAGE_DOWN
:
263 case REQ_SCROLL_PAGE_UP
:
264 case REQ_SCROLL_WHEEL_DOWN
:
265 case REQ_SCROLL_WHEEL_UP
:
266 scroll_view(view
, request
);
274 case REQ_VIEW_BRANCH
:
277 case REQ_VIEW_STATUS
:
281 open_view(view
, request
, OPEN_DEFAULT
);
290 line
= view
->pos
.lineno
;
291 view_request(view
, request
);
292 move_view(view
, request
);
293 if (view_is_displayed(view
))
294 update_view_title(view
);
295 if (line
!= view
->pos
.lineno
)
296 view_request(view
, REQ_ENTER
);
298 move_view(view
, request
);
304 int nviews
= displayed_views();
305 int next_view
= (current_view
+ 1) % nviews
;
307 if (next_view
== current_view
) {
308 report("Only one view is displayed");
312 current_view
= next_view
;
313 /* Blur out the title of the previous view. */
314 update_view_title(view
);
319 report("Refreshing is not supported by the %s view", view
->name
);
323 report("Moving to parent is not supported by the the %s view", view
->name
);
327 report("Going back is not supported for by %s view", view
->name
);
331 if (displayed_views() == 2)
332 maximize_view(view
, TRUE
);
336 case REQ_TOGGLE_LINENO
:
337 case REQ_TOGGLE_DATE
:
338 case REQ_TOGGLE_AUTHOR
:
339 case REQ_TOGGLE_FILENAME
:
340 case REQ_TOGGLE_GRAPHIC
:
341 case REQ_TOGGLE_REV_GRAPH
:
342 case REQ_TOGGLE_REFS
:
343 case REQ_TOGGLE_CHANGES
:
344 case REQ_TOGGLE_IGNORE_SPACE
:
346 case REQ_TOGGLE_FILES
:
347 case REQ_TOGGLE_TITLE_OVERFLOW
:
348 case REQ_TOGGLE_FILE_SIZE
:
349 case REQ_TOGGLE_UNTRACKED_DIRS
:
350 case REQ_TOGGLE_VERTICAL_SPLIT
:
352 char action
[SIZEOF_STR
] = "";
353 enum view_flag flags
= toggle_option(view
, request
, action
);
355 if (flags
== VIEW_FLAG_RESET_DISPLAY
) {
357 redraw_display(TRUE
);
359 foreach_displayed_view(view
, i
) {
360 if (view_has_flags(view
, flags
) && !view
->unrefreshable
)
368 report("%s", action
);
372 case REQ_TOGGLE_SORT_FIELD
:
373 case REQ_TOGGLE_SORT_ORDER
:
374 report("Sorting is not yet supported for the %s view", view
->name
);
377 case REQ_DIFF_CONTEXT_UP
:
378 case REQ_DIFF_CONTEXT_DOWN
:
379 report("Changing the diff context is not yet supported for the %s view", view
->name
);
383 case REQ_SEARCH_BACK
:
384 search_view(view
, request
);
389 find_next(view
, request
);
392 case REQ_STOP_LOADING
:
393 foreach_view(view
, i
) {
395 report("Stopped loading the %s view", view
->name
),
396 end_update(view
, TRUE
);
400 case REQ_SHOW_VERSION
:
401 report("tig-%s (built %s)", TIG_VERSION
, __DATE__
);
404 case REQ_SCREEN_REDRAW
:
405 redraw_display(TRUE
);
409 report("Nothing to edit");
413 report("Nothing to enter");
417 /* XXX: Mark closed views by letting view->prev point to the
418 * view itself. Parents to closed view should never be
420 if (view
->prev
&& view
->prev
!= view
) {
421 maximize_view(view
->prev
, TRUE
);
430 report("Unknown key, press %s for help",
431 get_view_key(view
, REQ_VIEW_HELP
));
442 static const char usage_string
[] =
443 "tig " TIG_VERSION
" (" __DATE__
")\n"
445 "Usage: tig [options] [revs] [--] [paths]\n"
446 " or: tig log [options] [revs] [--] [paths]\n"
447 " or: tig show [options] [revs] [--] [paths]\n"
448 " or: tig blame [options] [rev] [--] path\n"
451 " or: tig < [git command output]\n"
454 " +<number> Select line <number> in the first view\n"
455 " -v, --version Show version and exit\n"
456 " -h, --help Show help message and exit";
459 usage(const char *message
)
461 die("%s\n\n%s", message
, usage_string
);
465 read_filter_args(char *name
, size_t namelen
, char *value
, size_t valuelen
, void *data
)
467 const char ***filter_args
= data
;
469 return argv_append(filter_args
, name
) ? OK
: ERR
;
473 filter_rev_parse(const char ***args
, const char *arg1
, const char *arg2
, const char *argv
[])
475 const char *rev_parse_argv
[SIZEOF_ARG
] = { "git", "rev-parse", arg1
, arg2
};
476 const char **all_argv
= NULL
;
478 if (!argv_append_array(&all_argv
, rev_parse_argv
) ||
479 !argv_append_array(&all_argv
, argv
) ||
480 io_run_load(all_argv
, "\n", read_filter_args
, args
) == ERR
)
481 die("Failed to split arguments");
487 is_rev_flag(const char *flag
)
489 static const char *rev_flags
[] = { GIT_REV_FLAGS
};
492 for (i
= 0; i
< ARRAY_SIZE(rev_flags
); i
++)
493 if (!strcmp(flag
, rev_flags
[i
]))
500 filter_options(const char *argv
[])
502 const char **flags
= NULL
;
505 update_options_from_argv(argv
);
507 filter_rev_parse(&opt_file_argv
, "--no-revs", "--no-flags", argv
);
508 filter_rev_parse(&flags
, "--flags", "--no-revs", argv
);
511 for (next
= flags_pos
= 0; flags
&& flags
[next
]; next
++) {
512 const char *flag
= flags
[next
];
514 if (is_rev_flag(flag
))
515 argv_append(&opt_rev_argv
, flag
);
517 flags
[flags_pos
++] = flag
;
520 flags
[flags_pos
] = NULL
;
522 opt_cmdline_argv
= flags
;
525 filter_rev_parse(&opt_rev_argv
, "--symbolic", "--revs-only", argv
);
529 parse_options(int argc
, const char *argv
[], bool pager_mode
)
531 enum request request
;
532 const char *subcommand
;
533 bool seen_dashdash
= FALSE
;
534 const char **filter_argv
= NULL
;
537 request
= pager_mode
? REQ_VIEW_PAGER
: REQ_VIEW_MAIN
;
542 subcommand
= argv
[1];
543 if (!strcmp(subcommand
, "status")) {
544 request
= REQ_VIEW_STATUS
;
546 } else if (!strcmp(subcommand
, "blame")) {
547 request
= REQ_VIEW_BLAME
;
549 } else if (!strcmp(subcommand
, "show")) {
550 request
= REQ_VIEW_DIFF
;
552 } else if (!strcmp(subcommand
, "log")) {
553 request
= REQ_VIEW_LOG
;
555 } else if (!strcmp(subcommand
, "stash")) {
556 request
= REQ_VIEW_STASH
;
562 for (i
= 1 + !!subcommand
; i
< argc
; i
++) {
563 const char *opt
= argv
[i
];
565 // stop parsing our options after -- and let rev-parse handle the rest
566 if (!seen_dashdash
) {
567 if (!strcmp(opt
, "--")) {
568 seen_dashdash
= TRUE
;
571 } else if (!strcmp(opt
, "-v") || !strcmp(opt
, "--version")) {
572 printf("tig version %s\n", TIG_VERSION
);
575 } else if (!strcmp(opt
, "-h") || !strcmp(opt
, "--help")) {
576 printf("%s\n", usage_string
);
579 } else if (strlen(opt
) >= 2 && *opt
== '+' && string_isnumber(opt
+ 1)) {
580 int lineno
= atoi(opt
+ 1);
582 view_env
.lineno
= lineno
> 0 ? lineno
- 1 : 0;
588 if (!argv_append(&filter_argv
, opt
))
589 die("command too long");
593 filter_options(filter_argv
);
599 open_pager_mode(enum request request
)
601 enum open_flags flags
= OPEN_DEFAULT
;
603 if (request
== REQ_VIEW_PAGER
) {
604 /* Detect if the user requested the main view. */
605 if (argv_contains(opt_rev_argv
, "--stdin")) {
606 request
= REQ_VIEW_MAIN
;
607 flags
|= OPEN_FORWARD_STDIN
;
608 } else if (argv_contains(opt_cmdline_argv
, "--pretty=raw")) {
609 request
= REQ_VIEW_MAIN
;
615 } else if (request
== REQ_VIEW_DIFF
) {
616 if (argv_contains(opt_rev_argv
, "--stdin"))
617 flags
|= OPEN_FORWARD_STDIN
;
620 /* Open the requested view even if the pager mode is enabled so
621 * the warning message below is displayed correctly. */
622 open_view(NULL
, request
, flags
);
624 if (!open_in_pager_mode(flags
)) {
626 report("Ignoring stdin.");
633 run_prompt_command(struct view
*view
, char *cmd
)
635 enum request request
;
637 if (cmd
&& string_isnumber(cmd
)) {
638 int lineno
= view
->pos
.lineno
+ 1;
640 if (parse_int(&lineno
, cmd
, 1, view
->lines
+ 1) == SUCCESS
) {
641 select_view_line(view
, lineno
- 1);
644 report("Unable to parse '%s' as a line number", cmd
);
646 } else if (cmd
&& iscommit(cmd
)) {
647 string_ncopy(view
->env
->search
, cmd
, strlen(cmd
));
649 request
= view_request(view
, REQ_JUMP_COMMIT
);
650 if (request
== REQ_JUMP_COMMIT
) {
651 report("Jumping to commits is not supported by the '%s' view", view
->name
);
654 } else if (cmd
&& strlen(cmd
) == 1) {
655 request
= get_keybinding(&view
->ops
->keymap
, cmd
[0]);
658 } else if (cmd
&& cmd
[0] == '!') {
659 struct view
*next
= VIEW(REQ_VIEW_PAGER
);
660 const char *argv
[SIZEOF_ARG
];
664 /* When running random commands, initially show the
665 * command in the title. However, it maybe later be
666 * overwritten if a commit line is selected. */
667 string_ncopy(next
->ref
, cmd
, strlen(cmd
));
669 if (!argv_from_string(argv
, &argc
, cmd
)) {
670 report("Too many arguments");
671 } else if (!format_argv(view
->env
, &next
->argv
, argv
, FALSE
, TRUE
)) {
672 report("Argument formatting failed");
675 open_view(view
, REQ_VIEW_PAGER
, OPEN_PREPARED
);
679 request
= get_request(cmd
);
680 if (request
!= REQ_UNKNOWN
)
683 char *args
= strchr(cmd
, ' ');
686 if (set_option(cmd
, args
) == SUCCESS
) {
687 request
= !view
->unrefreshable
? REQ_REFRESH
: REQ_SCREEN_REDRAW
;
688 if (!strcmp(cmd
, "color"))
697 #ifdef NCURSES_MOUSE_VERSION
699 find_clicked_view(MEVENT
*event
)
704 foreach_displayed_view (view
, i
) {
705 int beg_y
= 0, beg_x
= 0;
707 getbegyx(view
->win
, beg_y
, beg_x
);
709 if (beg_y
<= event
->y
&& event
->y
< beg_y
+ view
->height
710 && beg_x
<= event
->x
&& event
->x
< beg_x
+ view
->width
) {
711 if (i
!= current_view
) {
722 handle_mouse_event(void)
727 if (getmouse(&event
) != OK
)
730 view
= find_clicked_view(&event
);
734 if (event
.bstate
& BUTTON2_PRESSED
)
735 return REQ_SCROLL_WHEEL_DOWN
;
737 if (event
.bstate
& BUTTON4_PRESSED
)
738 return REQ_SCROLL_WHEEL_UP
;
740 if (event
.bstate
& BUTTON1_PRESSED
) {
741 if (event
.y
== view
->pos
.lineno
- view
->pos
.offset
) {
742 /* Click is on the same line, perform an "ENTER" */
746 int y
= getbegy(view
->win
);
747 unsigned long lineno
= (event
.y
- y
) + view
->pos
.offset
;
749 select_view_line(view
, lineno
);
750 update_view_title(view
);
760 main(int argc
, const char *argv
[])
762 const char *codeset
= ENCODING_UTF8
;
763 bool pager_mode
= !isatty(STDIN_FILENO
);
764 enum request request
= parse_options(argc
, argv
, pager_mode
);
768 signal(SIGPIPE
, SIG_IGN
);
770 if (setlocale(LC_ALL
, "")) {
771 codeset
= nl_langinfo(CODESET
);
774 foreach_view(view
, i
) {
775 add_keymap(&view
->ops
->keymap
);
778 if (load_repo_info() == ERR
)
779 die("Failed to load repo info.");
781 if (load_options() == ERR
)
782 die("Failed to load user config.");
784 if (load_git_config() == ERR
)
785 die("Failed to load repo config.");
787 /* Require a git repository unless when running in pager mode. */
788 if (!repo
.git_dir
[0] && request
!= REQ_VIEW_PAGER
)
789 die("Not a git repository");
791 if (codeset
&& strcmp(codeset
, ENCODING_UTF8
)) {
792 char translit
[SIZEOF_STR
];
794 if (string_format(translit
, "%s%s", codeset
, ICONV_TRANSLIT
))
795 opt_iconv_out
= iconv_open(translit
, ENCODING_UTF8
);
797 opt_iconv_out
= iconv_open(codeset
, ENCODING_UTF8
);
798 if (opt_iconv_out
== ICONV_NONE
)
799 die("Failed to initialize character set conversion");
802 if (load_refs(FALSE
) == ERR
)
803 die("Failed to load refs.");
808 request
= open_pager_mode(request
);
810 while (view_driver(display
[current_view
], request
)) {
811 int key
= get_input(0);
813 #ifdef NCURSES_MOUSE_VERSION
814 if (key
== KEY_MOUSE
) {
815 request
= handle_mouse_event();
821 key
= get_input(0) + 0x80;
823 view
= display
[current_view
];
824 request
= get_keybinding(&view
->ops
->keymap
, key
);
826 /* Some low-level request handling. This keeps access to
827 * status_win restricted. */
830 report("Unknown key, press %s for help",
831 get_view_key(view
, REQ_VIEW_HELP
));
835 char *cmd
= read_prompt(":");
836 request
= run_prompt_command(view
, cmd
);
840 case REQ_SEARCH_BACK
:
842 const char *prompt
= request
== REQ_SEARCH
? "/" : "?";
843 char *search
= read_prompt(prompt
);
846 string_ncopy(view_env
.search
, search
, strlen(search
));
847 else if (*view_env
.search
)
848 request
= request
== REQ_SEARCH
?
865 /* vim: set ts=8 sw=8 noexpandtab: */