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

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

Download raw body.

Thread
On 22-09-15 01:45AM, Mark Jamsek wrote:
> On 22-09-14 05:30PM, Stefan Sperling wrote:
> > On Thu, Sep 15, 2022 at 01:08:53AM +1000, Mark Jamsek wrote:
> > > 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'?
> 
> > I would suggest to provide a visual indication at the bottom of the
> > help view, to indicate whether scrolling downwards is possible.
> > In an 80x24 terminal only the global options are visible initially, and
> > scrolling is required to make any view-specific keys appear. It should
> > somehow be made very obvious that the help window can be scrolled.
> > 
> > E.g. the last line of the help view could be reserved for a text string such
> > as "scroll down for more", or a percentage indicator, or similar. And once
> > the bottom of the view been reached such text could be replaced with the line
> > "See tog(1) for full documentation" (or perhaps "See the tog(1) manual page
> > for full documentation" would be clearer).
> > That's just one idea, perhaps there are better ways to do this.
> 
> That's a good idea! I like the percentage indicator, I think I'll go
> with that and see how it feels. Or maybe even both, a percentage and
> a comment. I'll have a play and shoot through an update.

I tried a few different things, most notably scroll bars and stsp's
suggestion. The scroll bars were interesting but ultimately look out of
place in a TUI app, imo. So I settled on a highlighted comment and
percentage indicator: "scroll down for more...[mn%]"

I think this both provides a good cue to the user and fits the general
tog aesthetic. If you'd like to see the scroll bar version, let me know
and I'll shoot through the diff.

As previously mentioned, I'd like to add backwards search via the '?'
key map to tog, so I've only documented H and F1 but runtime help is
still accessible via ? for now.

diff refs/heads/main refs/heads/dev/toghelp2
commit - fa61bdcb23187d65bfa426507dac0cae53857d7c
commit + 2c49ad376bb56e78c39f108a8118925ca90aef89
blob - 96b463e5b3dde2c8646f1153e008d0331518d2f7
blob + 42e7a83ecb03b5864f54b8d755d9073ec0a182c7
--- tog/tog.1
+++ tog/tog.1
@@ -69,6 +69,13 @@ or
 .Pp
 The global key bindings are:
 .Bl -tag -width Ds
+.It Cm H, F1
+Display run-time help.
+Key bindings for the focussed view will be displayed.
+Pressing this again inside the help view will toggle the display of
+key bindings for all
+.Nm
+views.
 .It Cm Q
 Quit
 .Nm .
blob - 3bb7704fbbba7e7851db72c19e9a7bbd520ae5db
blob + b11f730773e3daa0c7f7d516291ac268215a3ecf
--- 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,501 @@ 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;
+		size_t	 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, len = width;
+
+	if (km->keys) {
+		char	*t0, *t, *k;
+		int	 cs, s, first = 1;
+
+		cs = got_locale_is_utf8();
+
+		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, "%*s%s\n", width - len, width - len ? " " : "", 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) {
+			show = s->all;
+			if (km[i].type == TOG_KEYMAP_GLOBAL ||
+			    km[i].type == s->type || s->all)
+				show = 1;
+		}
+		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;
+	const struct got_error		*err;
+	regmatch_t			*regmatch = &view->regmatch;
+	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) {
+		rc = waddnstr(view->window,
+		    "See the tog(1) manual page for full documentation",
+		    view->ncols - 1);
+		if (rc == ERR)
+			return got_error_msg(GOT_ERR_RANGE, "waddnstr");
+	} else {
+		wmove(view->window, view->nlines - 1, 0);
+		wclrtoeol(view->window);
+		wstandout(view->window);
+		rc = waddnstr(view->window, "scroll down for more...",
+		    view->ncols - 1);
+		if (rc == ERR)
+			return got_error_msg(GOT_ERR_RANGE, "waddnstr");
+		if (getcurx(view->window) < view->ncols - 6) {
+			rc = wprintw(view->window, "[%.0f%%]",
+			    100.00 * s->last_displayed_line / s->nlines);
+			if (rc == ERR)
+				return got_error_msg(GOT_ERR_IO, "wprintw");
+		}
+		wstandend(view->window);
+	}
+
+	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 +8985,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