"GOT", but the "O" is a cute, smiling pufferfish. Index | Thread | Search

From:
Mark Jamsek <mark@jamsek.com>
Subject:
tog: runtime help
To:
Game of Trees <gameoftrees@openbsd.org>
Date:
Thu, 15 Sep 2022 01:08:53 +1000

Download raw body.

Thread
I started this the last Saturday of the hackathon after floating the
idea to tb@ who also thought it could be useful (similar to less(1) 'h')
and just remembered today as I was cleaning out old branches!  We have
a lot of key maps, and most TUI apps I use provide some runtime
reference for this (e.g., less(1), mutt, hackernews), so I thought it
would add value to tog too.

I've just finished rewriting the implementation because the initial code
was too naive and somewhat inconsistent with the other views;
we initially used a curses pad, which did 80% of what we expect, but as
I read it over, I realised we'd have to write a lot more code to add
functionality for which we already have code (e.g., search, splits,
resize) that could instead be reused if I just changed the implementation.
Plus, it now looks more consistent with the other tog views rather than
the initial popup dialog box. And I thought being able to have the help
view open in a split will help new users get familiar with tog.

I spoke to stsp and tobhe too. tobhe also thought it would be handy, and
stsp was concerned wrt keeping it in sync, so I've tried to make it
simple to update keymaps with some x-macros. stsp also suggested it
should be view-specific; so if you hit '?' in log view, just display log
key maps, diff keys in diff view, etc.  We've done this, but also made it
so that hitting '?' again in the help view will toggle display of all
key maps. But I'm happy to drop this if you think it's too much, so
I haven't documented anything in the man page yet because I wanted to
get some feedback first.

The diff is probably larger than you'd expect, but a fair bit of this is
due to refactoring the search code; we've deduped the
search_next_{blame,diff}_view, and reused this for the help search. So
this will ultimately be committed separately if ok. I've left it as is
because we reuse the refactored code for the help view, but if you
prefer, I can split the diff up now for review.

One pertinent question I have is:

  I'd eventually like to add '?' to start backwards search like vi(1);
  if in agreement, should we not use '?' now to toggle help and instead
  just stick with 'H' and 'F1'?

diff refs/heads/main refs/heads/dev/toghelp2
commit - f83ada346bec1ad98dda1d8f0ee7e95551ff4780
commit + f0c843e92e031ae218b24c3dcedeec99e1b116f6
blob - 3bb7704fbbba7e7851db72c19e9a7bbd520ae5db
blob + cfeb8e0b26c98dac9690505f99912f3a86bc4af3
--- tog/tog.c
+++ tog/tog.c
@@ -103,8 +103,21 @@ enum tog_view_type {
 	TOG_VIEW_BLAME,
 	TOG_VIEW_TREE,
 	TOG_VIEW_REF,
+	TOG_VIEW_HELP
 };
 
+/* Match _DIFF to _HELP with enum tog_view_type TOG_VIEW_* counterparts. */
+enum tog_keymap_type {
+	TOG_KEYMAP_KEYS = -2,
+	TOG_KEYMAP_GLOBAL,
+	TOG_KEYMAP_DIFF,
+	TOG_KEYMAP_LOG,
+	TOG_KEYMAP_BLAME,
+	TOG_KEYMAP_TREE,
+	TOG_KEYMAP_REF,
+	TOG_KEYMAP_HELP
+};
+
 enum tog_view_mode {
 	TOG_VIEW_SPLIT_NONE,
 	TOG_VIEW_SPLIT_VERT,
@@ -497,6 +510,104 @@ struct tog_ref_view_state {
 	struct tog_colors colors;
 };
 
+struct tog_help_view_state {
+	FILE			*f;
+	off_t			*line_offsets;
+	size_t			 nlines;
+	int			 lineno;
+	int			 first_displayed_line;
+	int			 last_displayed_line;
+	int			 eof;
+	int			 matched_line;
+	int			 selected_line;
+	int			 all;
+	enum tog_keymap_type	 type;
+};
+
+#define GENERATE_HELP \
+	KEYMAP_("Global", TOG_KEYMAP_GLOBAL), \
+	KEY_("H F1 ?", "Open view-specific help (double tap for all help)"), \
+	KEY_("k C-p Up", "Move cursor or page up one line"), \
+	KEY_("j C-n Down", "Move cursor or page down one line"), \
+	KEY_("C-b b PgUp", "Scroll the view up one page"), \
+	KEY_("C-f f PgDn Space", "Scroll the view down one page"), \
+	KEY_("C-u u", "Scroll the view up one half page"), \
+	KEY_("C-d d", "Scroll the view down one half page"), \
+	KEY_("g Home", "Go to line N (default: first line)"), \
+	KEY_("G End", "Go to line N (default: last line)"), \
+	KEY_("l Right", "Scroll the view right"), \
+	KEY_("h Left", "Scroll the view left"), \
+	KEY_("$", "Scroll view to the rightmost position"), \
+	KEY_("0", "Scroll view to the leftmost position"), \
+	KEY_("-", "Decrease size of the focussed split"), \
+	KEY_("+", "Increase size of the focussed split"), \
+	KEY_("Tab", "Switch focus between views"), \
+	KEY_("F", "Toggle fullscreen mode"), \
+	KEY_("/", "Open prompt to enter search term"), \
+	KEY_("n", "Find next line/token matching the current search term"), \
+	KEY_("N", "Find previous line/token matching the current search term"),\
+	KEY_("q", "Quit the focussed view"), \
+	KEY_("Q", "Quit tog"), \
+	\
+	KEYMAP_("Log", TOG_KEYMAP_LOG), \
+	KEY_("< ,", "Move cursor up one commit"), \
+	KEY_("> .", "Move cursor down one commit"), \
+	KEY_("Enter", "Open diff view of the selected commit"), \
+	KEY_("B", "Reload the log view and toggle display of merged commits"), \
+	KEY_("R", "Open ref view of all repository references"), \
+	KEY_("T", "Display tree view of the repository from the selected" \
+	    " commit"), \
+	KEY_("@", "Toggle between displaying author and committer name"), \
+	KEY_("&", "Open prompt to enter term to limit commits displayed"), \
+	KEY_("C-g Backspace", "Cancel current search or log operation"), \
+	KEY_("C-l", "Reload the log view with new commits in the repository"), \
+	\
+	KEYMAP_("Diff", TOG_KEYMAP_DIFF), \
+	KEY_("K < ,", "Display diff of next line in the file/log entry"), \
+	KEY_("J > .", "Display diff of previous line in the file/log entry"), \
+	KEY_("A", "Toggle between Myers and Patience diff algorithm"), \
+	KEY_("a", "Toggle treatment of file as ASCII irrespective of binary" \
+	    " data"), \
+	KEY_("(", "Go to the previous file in the diff"), \
+	KEY_(")", "Go to the next file in the diff"), \
+	KEY_("{", "Go to the previous hunk in the diff"), \
+	KEY_("}", "Go to the next hunk in the diff"), \
+	KEY_("[", "Decrease the number of context lines"), \
+	KEY_("]", "Increase the number of context lines"), \
+	KEY_("w", "Toggle ignore whitespace-only changes in the diff"), \
+	\
+	KEYMAP_("Blame", TOG_KEYMAP_BLAME), \
+	KEY_("Enter", "Display diff view of the selected line's commit"), \
+	KEY_("A", "Toggle diff algorithm between Myers and Patience"), \
+	KEY_("L", "Open log view for the currently selected annotated line"), \
+	KEY_("C", "Reload view with the previously blamed commit"), \
+	KEY_("c", "Reload view with the version of the file found in the" \
+	    " selected line's commit"), \
+	KEY_("p", "Reload view with the version of the file found in the" \
+	    " selected line's parent commit"), \
+	\
+	KEYMAP_("Tree", TOG_KEYMAP_TREE), \
+	KEY_("Enter", "Enter selected directory or open blame view of the" \
+	    " selected file"), \
+	KEY_("L", "Open log view for the selected entry"), \
+	KEY_("R", "Open ref view of all repository references"), \
+	KEY_("i", "Show object IDs for all tree entries"), \
+	KEY_("Backspace", "Return to the parent directory"), \
+	\
+	KEYMAP_("Ref", TOG_KEYMAP_REF), \
+	KEY_("Enter", "Display log view of the selected reference"), \
+	KEY_("T", "Display tree view of the selected reference"), \
+	KEY_("i", "Toggle display of IDs for all non-symbolic references"), \
+	KEY_("m", "Toggle display of last modified date for each reference"), \
+	KEY_("o", "Toggle reference sort order (name -> timestamp)"), \
+	KEY_("C-l", "Reload view with all repository references")
+
+struct tog_key_map {
+	const char		*keys;
+	const char		*info;
+	enum tog_keymap_type	 type;
+};
+
 /*
  * We implement two types of views: parent views and child views.
  *
@@ -553,6 +664,7 @@ struct tog_view {
 		struct tog_blame_view_state blame;
 		struct tog_tree_view_state tree;
 		struct tog_ref_view_state ref;
+		struct tog_help_view_state help;
 	} state;
 
 	const struct got_error *(*show)(struct tog_view *);
@@ -586,7 +698,7 @@ static const struct got_error *input_diff_view(struct 
 static const struct got_error *reset_diff_view(struct tog_view *);
 static const struct got_error* close_diff_view(struct tog_view *);
 static const struct got_error *search_start_diff_view(struct tog_view *);
-static const struct got_error *search_next_diff_view(struct tog_view *);
+static const struct got_error *search_next_view_match(struct tog_view *);
 
 static const struct got_error *open_log_view(struct tog_view *,
     struct got_object_id *, struct got_repository *,
@@ -607,7 +719,6 @@ static const struct got_error *input_blame_view(struct
 static const struct got_error *reset_blame_view(struct tog_view *);
 static const struct got_error *close_blame_view(struct tog_view *);
 static const struct got_error *search_start_blame_view(struct tog_view *);
-static const struct got_error *search_next_blame_view(struct tog_view *);
 
 static const struct got_error *open_tree_view(struct tog_view *,
     struct got_object_id *, const char *, struct got_repository *);
@@ -1410,6 +1521,14 @@ view_input(struct tog_view **new, int *done, struct to
 	}
 
 	switch (ch) {
+	case '?':
+	case 'H':
+	case KEY_F(1):
+		if (view->type == TOG_VIEW_HELP)
+			err = view->reset(view);
+		else
+			err = view_request_new(new, view, TOG_VIEW_HELP);
+		break;
 	case '\t':
 		view->count = 0;
 		if (view->child) {
@@ -4098,6 +4217,13 @@ gotoline(struct tog_view *view, int *lineno, int *npri
 		selected = first;
 		eof = &s->eof;
 		f = s->f;
+	} else if (view->type == TOG_VIEW_HELP) {
+		struct tog_help_view_state *s = &view->state.help;
+
+		first = &s->first_displayed_line;
+		selected = first;
+		eof = &s->eof;
+		f = s->f;
 	} else if (view->type == TOG_VIEW_BLAME) {
 		struct tog_blame_view_state *s = &view->state.blame;
 
@@ -4645,33 +4771,93 @@ search_start_diff_view(struct tog_view *view)
 }
 
 static const struct got_error *
-search_next_diff_view(struct tog_view *view)
+search_set_view(struct tog_view *view, FILE **f, off_t **line_offsets,
+    size_t *nlines, int **first, int **last, int **match, int **selected)
 {
-	struct tog_diff_view_state *s = &view->state.diff;
+	*f = NULL;
+	*first = *last = *match = *selected = NULL;
+	*line_offsets = NULL;
+
+	switch (view->type) {
+	case (TOG_VIEW_DIFF): {
+		struct tog_diff_view_state *s = &view->state.diff;
+
+		*f = s->f;
+		*nlines = s->nlines;
+		*match = &s->matched_line;
+		*first = &s->first_displayed_line;
+		*last = &s->last_displayed_line;
+		*selected = &s->selected_line;
+		break;
+	}
+	case (TOG_VIEW_BLAME): {
+		struct tog_blame_view_state *s = &view->state.blame;
+
+		*f = s->blame.f;
+		*nlines = s->blame.nlines;
+		*line_offsets = s->blame.line_offsets;
+		*match = &s->matched_line;
+		*first = &s->first_displayed_line;
+		*last = &s->last_displayed_line;
+		*selected = &s->selected_line;
+		break;
+	}
+	case (TOG_VIEW_HELP): {
+		struct tog_help_view_state *s = &view->state.help;
+
+		*f = s->f;
+		*nlines = s->nlines;
+		*line_offsets = s->line_offsets;
+		*match = &s->matched_line;
+		*first = &s->first_displayed_line;
+		*last = &s->last_displayed_line;
+		*selected = &s->selected_line;
+		break;
+	}
+	default:
+		return got_error_msg(GOT_ERR_NOT_IMPL,
+		    "view search not supported");
+	}
+
+	return NULL;
+}
+
+static const struct got_error *
+search_next_view_match(struct tog_view *view)
+{
 	const struct got_error *err = NULL;
+	FILE *f;
 	int lineno;
 	char *line = NULL;
 	size_t linesize = 0;
 	ssize_t linelen;
+	off_t *line_offsets;
+	size_t nlines = 0;
+	int *first, *last, *match, *selected;
 
+	err = search_set_view(view, &f, &line_offsets, &nlines, &first, &last,
+	    &match, &selected);
+	if (err)
+		return err;
+
 	if (!view->searching) {
 		view->search_next_done = TOG_SEARCH_HAVE_MORE;
 		return NULL;
 	}
 
-	if (s->matched_line) {
+	if (*match) {
 		if (view->searching == TOG_SEARCH_FORWARD)
-			lineno = s->matched_line + 1;
+			lineno = *match + 1;
 		else
-			lineno = s->matched_line - 1;
+			lineno = *match - 1;
 	} else
-		lineno = s->first_displayed_line;
+		lineno = *first - 1 + *selected;
 
 	while (1) {
 		off_t offset;
 
-		if (lineno <= 0 || lineno > s->nlines) {
-			if (s->matched_line == 0) {
+		if (lineno <= 0 || lineno > nlines) {
+			if (*match == 0) {
 				view->search_next_done = TOG_SEARCH_HAVE_MORE;
 				break;
 			}
@@ -4679,15 +4865,17 @@ search_next_diff_view(struct tog_view *view)
 			if (view->searching == TOG_SEARCH_FORWARD)
 				lineno = 1;
 			else
-				lineno = s->nlines;
+				lineno = nlines;
 		}
 
-		offset = s->lines[lineno - 1].offset;
-		if (fseeko(s->f, offset, SEEK_SET) != 0) {
+		offset = view->type == TOG_VIEW_DIFF ?
+		    view->state.diff.lines[lineno - 1].offset :
+		    line_offsets[lineno - 1];
+		if (fseeko(f, offset, SEEK_SET) != 0) {
 			free(line);
 			return got_error_from_errno("fseeko");
 		}
-		linelen = getline(&line, &linesize, s->f);
+		linelen = getline(&line, &linesize, f);
 		if (linelen != -1) {
 			char *exstr;
 			err = expand_tab(&exstr, line);
@@ -4696,7 +4884,7 @@ search_next_diff_view(struct tog_view *view)
 			if (match_line(exstr, &view->regex, 1,
 			    &view->regmatch)) {
 				view->search_next_done = TOG_SEARCH_HAVE_MORE;
-				s->matched_line = lineno;
+				*match = lineno;
 				free(exstr);
 				break;
 			}
@@ -4709,9 +4897,9 @@ search_next_diff_view(struct tog_view *view)
 	}
 	free(line);
 
-	if (s->matched_line) {
-		s->first_displayed_line = s->matched_line;
-		s->selected_line = 1;
+	if (*match) {
+		*first = *match;
+		*selected = 1;
 	}
 
 	return err;
@@ -4874,7 +5062,7 @@ open_diff_view(struct tog_view *view, struct got_objec
 	view->reset = reset_diff_view;
 	view->close = close_diff_view;
 	view->search_start = search_start_diff_view;
-	view->search_next = search_next_diff_view;
+	view->search_next = search_next_view_match;
 done:
 	if (err)
 		close_diff_view(view);
@@ -5917,7 +6105,7 @@ open_blame_view(struct tog_view *view, char *path,
 	view->reset = reset_blame_view;
 	view->close = close_blame_view;
 	view->search_start = search_start_blame_view;
-	view->search_next = search_next_blame_view;
+	view->search_next = search_next_view_match;
 
 	return run_blame(view);
 }
@@ -5953,79 +6141,6 @@ search_start_blame_view(struct tog_view *view)
 }
 
 static const struct got_error *
-search_next_blame_view(struct tog_view *view)
-{
-	struct tog_blame_view_state *s = &view->state.blame;
-	const struct got_error *err = NULL;
-	int lineno;
-	char *line = NULL;
-	size_t linesize = 0;
-	ssize_t linelen;
-
-	if (!view->searching) {
-		view->search_next_done = TOG_SEARCH_HAVE_MORE;
-		return NULL;
-	}
-
-	if (s->matched_line) {
-		if (view->searching == TOG_SEARCH_FORWARD)
-			lineno = s->matched_line + 1;
-		else
-			lineno = s->matched_line - 1;
-	} else
-		lineno = s->first_displayed_line - 1 + s->selected_line;
-
-	while (1) {
-		off_t offset;
-
-		if (lineno <= 0 || lineno > s->blame.nlines) {
-			if (s->matched_line == 0) {
-				view->search_next_done = TOG_SEARCH_HAVE_MORE;
-				break;
-			}
-
-			if (view->searching == TOG_SEARCH_FORWARD)
-				lineno = 1;
-			else
-				lineno = s->blame.nlines;
-		}
-
-		offset = s->blame.line_offsets[lineno - 1];
-		if (fseeko(s->blame.f, offset, SEEK_SET) != 0) {
-			free(line);
-			return got_error_from_errno("fseeko");
-		}
-		linelen = getline(&line, &linesize, s->blame.f);
-		if (linelen != -1) {
-			char *exstr;
-			err = expand_tab(&exstr, line);
-			if (err)
-				break;
-			if (match_line(exstr, &view->regex, 1,
-			    &view->regmatch)) {
-				view->search_next_done = TOG_SEARCH_HAVE_MORE;
-				s->matched_line = lineno;
-				free(exstr);
-				break;
-			}
-			free(exstr);
-		}
-		if (view->searching == TOG_SEARCH_FORWARD)
-			lineno++;
-		else
-			lineno--;
-	}
-	free(line);
-
-	if (s->matched_line) {
-		s->first_displayed_line = s->matched_line;
-		s->selected_line = 1;
-	}
-
-	return err;
-}
-
-static const struct got_error *
 show_blame_view(struct tog_view *view)
 {
 	const struct got_error *err = NULL;
@@ -8301,7 +8416,485 @@ done:
 	return error;
 }
 
+static const struct got_error*
+win_draw_center(WINDOW *win, size_t y, size_t x, size_t maxx, int focus,
+    const char *str)
+{
+	size_t len;
+
+	if (win == NULL)
+		win = stdscr;
+
+	len = strlen(str);
+	x = x ? x : maxx > len ? (maxx - len) / 2 : 0;
+
+	if (focus)
+		wstandout(win);
+	if (mvwprintw(win, y, x, "%s", str) == ERR)
+		return got_error_msg(GOT_ERR_RANGE, "mvwprintw");
+	if (focus)
+		wstandend(win);
+
+	return NULL;
+}
+
 static const struct got_error *
+add_line_offset(off_t **line_offsets, size_t *nlines, off_t off)
+{
+	off_t *p;
+
+	p = reallocarray(*line_offsets, *nlines + 1, sizeof(off_t));
+	if (p == NULL) {
+		free(*line_offsets);
+		*line_offsets = NULL;
+		return got_error_from_errno("reallocarray");
+	}
+
+	*line_offsets = p;
+	(*line_offsets)[*nlines] = off;
+	++(*nlines);
+	return NULL;
+}
+
+static const struct got_error *
+max_key_str(int *ret, const struct tog_key_map *km, size_t n)
+{
+	*ret = 0;
+
+	for (;n > 0; --n, ++km) {
+		char	*t0, *t, *k;
+		int	 len = 1;
+
+		if (km->keys == NULL)
+			continue;
+
+		t = t0 = strdup(km->keys);
+		if (t0 == NULL)
+			return got_error_from_errno("strdup");
+
+		len += strlen(t);
+		while ((k = strsep(&t, " ")) != NULL)
+			len += strlen(k) > 1 ? 2 : 0;
+		free(t0);
+		*ret = MAX(*ret, len);
+	}
+
+	return NULL;
+}
+
+/*
+ * Write keymap section headers, keys, and key info in km to f.
+ * Save line offset to *off. If terminal has UTF8 encoding enabled,
+ * wrap control and symbolic keys in guillemets, else use <>.
+ * For example (top=UTF8, bottom=ASCII):
+ * Global
+ *   k ❬C-p❭ ❬Up❭               Move cursor or page up one line
+ * Global
+ *   k <C-p> <Up>               Move cursor or page up one line
+ */
+static const struct got_error *
+format_help_line(off_t *off, FILE *f, const struct tog_key_map *km, int width)
+{
+	int n;
+
+	if (km->keys) {
+		char	*t0, *t, *k;
+		int	 cs, s, first = 1, len;
+
+		cs = got_locale_is_utf8() ? 1 : 0;
+
+		t = t0 = strdup(km->keys);
+		if (t0 == NULL)
+			return got_error_from_errno("strdup");
+
+		len = strlen(km->keys);
+		while ((k = strsep(&t, " ")) != NULL) {
+			s = strlen(k) > 1;  /* control or symbolic key */
+			n = fprintf(f, "%s%s%s%s%s", first ? "  " : "",
+			    cs && s ? "❬" : s ? "<" : "", k,
+			    cs && s ? "❭" : s ? ">" : "", t ? " " : "");
+			if (n < 0) {
+				free(t0);
+				return got_error_from_errno("fprintf");
+			}
+			first = 0;
+			len += s ? 2 : 0;
+			*off += n;
+		}
+		free(t0);
+		n = fprintf(f, "%*c%s\n", width - len, ' ', km->info);
+	} else
+		n = fprintf(f, "%s\n", km->info);
+	if (n < 0)
+		return got_error_from_errno("fprintf");
+	*off += n;
+
+	return NULL;
+}
+
+static const struct got_error *
+format_help(struct tog_help_view_state *s)
+{
+	const struct got_error		*err = NULL;
+	off_t				 off = 0;
+	int				 i, max, n, show = s->all;
+	static const struct tog_key_map	 km[] = {
+#define KEYMAP_(info, type)	{ NULL, (info), type }
+#define KEY_(keys, info)	{ (keys), (info), TOG_KEYMAP_KEYS }
+		GENERATE_HELP
+#undef KEYMAP_
+#undef KEY_
+	};
+
+	err = add_line_offset(&s->line_offsets, &s->nlines, 0);
+	if (err)
+		return err;
+
+	n = nitems(km);
+	err = max_key_str(&max, km, n);
+	if (err)
+		return err;
+
+	for (i = 0; i < n; ++i) {
+		if (km[i].keys == NULL) {
+			if (km[i].type == s->type ||
+			    km[i].type == TOG_KEYMAP_GLOBAL || s->all)
+				show = 1;
+			else
+				show = s->all;
+		}
+		if (show) {
+			err = format_help_line(&off, s->f, &km[i], max);
+			if (err)
+				return err;
+		}
+		err = add_line_offset(&s->line_offsets, &s->nlines, off);
+		if (err)
+			return err;
+	}
+	fputc('\n', s->f);
+	++off;
+	err = add_line_offset(&s->line_offsets, &s->nlines, off);
+
+	return err;
+}
+
+static const struct got_error *
+create_help(struct tog_help_view_state *s)
+{
+	FILE			*f;
+	const struct got_error	*err;
+
+	free(s->line_offsets);
+	s->line_offsets = NULL;
+	s->nlines = 0;
+
+	f = got_opentemp();
+	if (f == NULL)
+		return got_error_from_errno("got_opentemp");
+	s->f = f;
+
+	err = format_help(s);
+	if (err)
+		return err;
+
+	if (s->f && fflush(s->f) != 0)
+		return got_error_from_errno("fflush");
+
+	return NULL;
+}
+
+static const struct got_error *
+search_start_help_view(struct tog_view *view)
+{
+	view->state.help.matched_line = 0;
+	return NULL;
+}
+
+static const struct got_error *
+show_help_view(struct tog_view *view)
+{
+	struct tog_help_view_state	*s = &view->state.help;
+	regmatch_t			*regmatch = &view->regmatch;
+	const struct got_error		*err;
+	wchar_t				*wline;
+	char				*line;
+	ssize_t				 linelen;
+	size_t				 linesz = 0;
+	int				 width, nprinted = 0, rc = 0;
+	int				 eos = view->nlines;
+
+	if (view_is_hsplit_top(view))
+		--eos;  /* account for border */
+
+	s->lineno = 0;
+	rewind(s->f);
+	werase(view->window);
+
+	if (view->gline > s->nlines - 1)
+		view->gline = s->nlines - 1;
+
+	err = win_draw_center(view->window, 0, 0, view->ncols,
+	    view_needs_focus_indication(view), "tog help");
+	if (err)
+		return err;
+	if (eos <= 1)
+		return NULL;
+	waddstr(view->window, "\n\n");
+	eos -= 2;
+
+	s->eof = 0;
+	view->maxx = 0;
+	line = NULL;
+	while (eos > 0 && nprinted < eos) {
+		attr_t attr = 0;
+
+		linelen = getline(&line, &linesz, s->f);
+		if (linelen == -1) {
+			if (!feof(s->f)) {
+				free(line);
+				return got_ferror(s->f, GOT_ERR_IO);
+			}
+			s->eof = 1;
+			break;
+		}
+		if (++s->lineno < s->first_displayed_line)
+			continue;
+		if (view->gline && !gotoline(view, &s->lineno, &nprinted))
+			continue;
+		if (s->lineno == view->hiline)
+			attr = A_STANDOUT;
+
+		err = format_line(&wline, &width, NULL, line, 0, INT_MAX, 0,
+		    view->x ? 1 : 0);
+		if (err) {
+			free(line);
+			return err;
+		}
+		view->maxx = MAX(view->maxx, width);
+		free(wline);
+		wline = NULL;
+
+		if (attr)
+			wattron(view->window, attr);
+		if (s->first_displayed_line + nprinted == s->matched_line &&
+		    regmatch->rm_so >= 0 && regmatch->rm_so < regmatch->rm_eo) {
+			err = add_matched_line(&width, line, view->ncols - 1, 0,
+			    view->window, view->x, regmatch);
+			if (err) {
+				free(line);
+				return err;
+			}
+		} else {
+			int skip;
+
+			err = format_line(&wline, &width, &skip, line,
+			    view->x, view->ncols - 1, 0, view->x ? 1 : 0);
+			if (err) {
+				free(line);
+				return err;
+			}
+			rc = waddwstr(view->window, &wline[skip]);
+			free(wline);
+			wline = NULL;
+			if (rc == ERR)
+				return got_error_msg(GOT_ERR_IO, "waddwstr");
+		}
+		if (s->lineno == view->hiline) {
+			while (width++ < view->ncols)
+				waddch(view->window, ' ');
+		} else {
+			if (width <= view->ncols)
+				waddch(view->window, '\n');
+		}
+		if (attr)
+			wattroff(view->window, attr);
+		if (++nprinted == 1)
+			s->first_displayed_line = s->lineno;
+	}
+	free(line);
+	if (nprinted > 0)
+		s->last_displayed_line = s->first_displayed_line + nprinted - 1;
+	else
+		s->last_displayed_line = s->first_displayed_line;
+
+	view_border(view);
+
+	if (s->eof)
+		waddnstr(view->window, "See tog(1) for full documentation",
+		    view->ncols - 1);
+
+	return NULL;
+}
+
+static const struct got_error *
+input_help_view(struct tog_view **new_view, struct tog_view *view, int ch)
+{
+	struct tog_help_view_state	*s = &view->state.help;
+	const struct got_error		*err = NULL;
+	char				*line = NULL;
+	ssize_t				 linelen;
+	size_t				 linesz = 0;
+	int				 eos, nscroll;
+
+	eos = nscroll = view->nlines;
+	if (view_is_hsplit_top(view))
+		--eos;  /* border */
+
+	s->lineno = s->first_displayed_line - 1 + s->selected_line;
+
+	switch (ch) {
+	case '0':
+		view->x = 0;
+		break;
+	case '$':
+		view->x = MAX(view->maxx - view->ncols / 3, 0);
+		view->count = 0;
+		break;
+	case KEY_RIGHT:
+	case 'l':
+		if (view->x + view->ncols / 3 < view->maxx)
+			view->x += 2;
+		else
+			view->count = 0;
+		break;
+	case KEY_LEFT:
+	case 'h':
+		view->x -= MIN(view->x, 2);
+		if (view->x <= 0)
+			view->count = 0;
+		break;
+	case 'g':
+	case KEY_HOME:
+		s->first_displayed_line = 1;
+		view->count = 0;
+		break;
+	case 'G':
+	case KEY_END:
+		view->count = 0;
+		if (s->eof)
+			break;
+		s->first_displayed_line = (s->nlines - eos) + 3;
+		s->eof = 1;
+		break;
+	case 'k':
+	case KEY_UP:
+		if (s->first_displayed_line > 1)
+			--s->first_displayed_line;
+		else
+			view->count = 0;
+		break;
+	case CTRL('u'):
+	case 'u':
+		nscroll /= 2;
+		/* FALL THROUGH */
+	case KEY_PPAGE:
+	case CTRL('b'):
+	case 'b':
+		if (s->first_displayed_line == 1) {
+			view->count = 0;
+			break;
+		}
+		while (--nscroll > 0 && s->first_displayed_line > 1)
+			s->first_displayed_line--;
+		break;
+	case 'j':
+	case KEY_DOWN:
+	case CTRL('n'):
+		if (!s->eof)
+			++s->first_displayed_line;
+		else
+			view->count = 0;
+		break;
+	case CTRL('d'):
+	case 'd':
+		nscroll /= 2;
+		/* FALL THROUGH */
+	case KEY_NPAGE:
+	case CTRL('f'):
+	case 'f':
+	case ' ':
+		if (s->eof) {
+			view->count = 0;
+			break;
+		}
+		while (!s->eof && --nscroll > 0) {
+			linelen = getline(&line, &linesz, s->f);
+			s->first_displayed_line++;
+			if (linelen == -1) {
+				if (feof(s->f))
+					s->eof = 1;
+				else
+					err = got_ferror(s->f, GOT_ERR_IO);
+				break;
+			}
+		}
+		free(line);
+		break;
+	default:
+		view->count = 0;
+		break;
+	}
+
+	return err;
+}
+
+static const struct got_error *
+close_help_view(struct tog_view *view)
+{
+	struct tog_help_view_state *s = &view->state.help;
+
+	free(s->line_offsets);
+	s->line_offsets = NULL;
+	if (fclose(s->f) == EOF)
+		return got_error_from_errno("fclose");
+
+	return NULL;
+}
+
+static const struct got_error *
+reset_help_view(struct tog_view *view)
+{
+	struct tog_help_view_state *s = &view->state.help;
+
+
+	if (s->f && fclose(s->f) == EOF)
+		return got_error_from_errno("fclose");
+
+	wclear(view->window);
+	view->count = 0;
+	view->x = 0;
+	s->all = !s->all;
+	s->first_displayed_line = 1;
+	s->last_displayed_line = view->nlines;
+	s->matched_line = 0;
+
+	return create_help(s);
+}
+
+static const struct got_error *
+open_help_view(struct tog_view *view, struct tog_view *parent)
+{
+	const struct got_error		*err = NULL;
+	struct tog_help_view_state	*s = &view->state.help;
+
+	s->type = (enum tog_keymap_type)parent->type;
+	s->first_displayed_line = 1;
+	s->last_displayed_line = view->nlines;
+	s->selected_line = 1;
+
+	view->show = show_help_view;
+	view->input = input_help_view;
+	view->reset = reset_help_view;
+	view->close = close_help_view;
+	view->search_start = search_start_help_view;
+	view->search_next = search_next_view_match;
+
+	err = create_help(s);
+	return err;
+}
+
+static const struct got_error *
 view_dispatch_request(struct tog_view **new_view, struct tog_view *view,
     enum tog_view_type request, int y, int x)
 {
@@ -8376,6 +8969,14 @@ view_dispatch_request(struct tog_view **new_view, stru
 		if (err)
 			view_close(*new_view);
 		break;
+	case TOG_VIEW_HELP:
+		*new_view = view_open(0, 0, y, x, TOG_VIEW_HELP);
+		if (*new_view == NULL)
+			return got_error_from_errno("view_open");
+		err = open_help_view(*new_view, view);
+		if (err)
+			view_close(*new_view);
+		break;
 	default:
 		return got_error_msg(GOT_ERR_NOT_IMPL, "invalid view");
 	}

-- 
Mark Jamsek <fnc.bsdbox.org>
GPG: F2FF 13DE 6A06 C471 CA80  E6E2 2930 DC66 86EE CF68