Commit Diff


commit - 15e1b108be37e22089affc9e1165a3834c9259b7
commit + 1d08c280e4fb59f78de88a09aa7932e3013e1ce7
blob - 8fd6b69aad5ec4b5c2f16a83d35000248847ade6
blob + 455231379643b68424898cad35d772cfd869d525
--- configure.ac
+++ configure.ac
@@ -4,21 +4,18 @@ AC_PROG_CC
 
 PKG_PROG_PKG_CONFIG
 
-dnl AX_WITH_CURSES
-dnl if test "x$ax_cv_ncursesw" != xyes && test "x$ax_cv_ncurses" != xyes; then
-dnl 	AC_MSG_ERROR([requires either NcursesW or Ncurses library])
-dnl fi
+AC_SEARCH_LIBS([initscr], [ncursesw ncurses], [], [
+	AC_MSG_ERROR([requires either ncursesw or ncurses library])
+])
 
-AC_CHECK_LIB(ncursesw, initscr, [],
-	AC_CHECK_LIB(ncurses, initscr, [],
-		AC_MSG_ERROR([requires either ncursesw or ncurses library])))
+AC_CHECK_LIB(tls, tls_init, [], [
+	AC_MSG_ERROR([requires libtls])
+])
 
-AC_CHECK_LIB(tls, tls_init, [],
-	AC_MSG_ERROR([requires libtls]))
+AC_CHECK_LIB(event, event_init, [], [
+	AC_MSG_ERROR([requires libevent])
+])
 
-AC_CHECK_LIB(event, event_init, [],
-	AC_MSG_ERROR([requires libevent]))
-
 IMSG_LDFLAGS=-lutil
 AC_SUBST([IMSG_LDFLAGS])
 
blob - 7a68d17396de5e737c5f63fe556fe0730022f43b
blob + 3ff19e7ad790dfb9328cd07a0a6c07b5c34c2e6f
--- telescope.c
+++ telescope.c
@@ -217,8 +217,9 @@ void
 new_tab(void)
 {
 	struct tab	*tab;
-	const char	*url = "about:new";
-	/* const char	*url = "gemini://localhost.it/"; */
+	/* const char	*url = "about:new"; */
+	/* const char	*url = "gemini://localhost.it/scroll.txt"; */
+	const char	*url = "gemini://localhost.it/test.gmi";
 
 	if ((tab = calloc(1, sizeof(*tab))) == NULL)
 		die();
blob - 62c3fb3d9c04d0e9a9f20ab61f4ff9c1a0d92f87
blob + 88b28838e6b20443ee507a5789451fbb439d7d35
--- telescope.h
+++ telescope.h
@@ -84,6 +84,8 @@ struct parser {
 	TAILQ_HEAD(, line)	 head;
 };
 
+struct ui_state;
+
 extern TAILQ_HEAD(tabshead, tab) tabshead;
 struct tab {
 	struct parser		 page;
@@ -94,6 +96,8 @@ struct tab {
 	int			 code;
 	char			 meta[GEMINI_URL_LEN];
 	int			 redirect_count;
+
+	struct ui_state		*s;
 };
 
 extern struct event		 imsgev;
@@ -113,7 +117,7 @@ extern const char	*err_pages[70];
 
 /* ui.c */
 int		 ui_init(void);
-void		 ui_on_new_tab(struct tab*);
+int		 ui_on_new_tab(struct tab*);
 void		 ui_on_tab_refresh(struct tab*);
 void		 ui_end(void);
 
blob - 0a00d3b8723a3b9f23ea154ddf335499633adcec
blob + a32b231dcdacc9ff63d4d65cc5b19decb370585f
--- ui.c
+++ ui.c
@@ -14,6 +14,48 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
+/*
+ * Ncurses UI for telescope.
+ *
+ *
+ * Text wrapping
+ * =============
+ *
+ * There's a simple text wrapping algorithm.
+ *
+ * 1. if it's a line in a pre-formatted block:
+ *    a. hard wrap.
+ *    b. repeat
+ * 2. there is enough room for the next word?
+ *    a. yes: render it
+ *    b. no: break the current line.
+ *       i.  while there isn't enough space to draw the current
+ *           word, hard-wrap it
+ *       ii. draw the remainder of the current word (can be the
+ *           the entirely)
+ * 3. render the spaces after the word
+ *    a. but if there is not enough room break the line and
+ *       forget them
+ * 4. repeat
+ *
+ *
+ * Text scrolling
+ * ==============
+ *
+ * ncurses allows you to scroll a window, but when a line goes out of
+ * the visible area it's forgotten.  We keep a list of formatted lines
+ * (``visual lines'') that we know fits in the window, and draw them.
+ * This way is easy to scroll: just call wscrl and then render the
+ * first/last line!
+ *
+ * This means that on every resize we have to clear our list of lines
+ * and re-render everything.  A clever approach would be to do this
+ * ``on-demand''.
+ *
+ * TODO: make the text formatting on-demand.
+ *
+ */
+
 #include <telescope.h>
 
 #include <curses.h>
@@ -27,15 +69,161 @@
 
 #define CTRL(x)		((x)&0x1F)
 
+#define MIN(a, b) ((a) < (b) ? (a) : (b))
+#define MAX(a, b) ((a) > (b) ? (a) : (b))
+
 static struct event	stdioev, winchev;
 
+static int		 push_line(struct tab*, const struct line*, const char*, size_t);
+static void		 empty_vlist(struct tab*);
 static struct tab	*current_tab(void);
 static void		 dispatch_stdio(int, short, void*);
 static void		 handle_resize(int, short, void*);
 static int		 word_bourdaries(const char*, const char*, const char**, const char**);
-static void		 wrap_text(const char*, const char*, const char*, const char*);
+static void		 wrap_text(struct tab*, const char*, struct line*);
+static int		 hardwrap_text(struct tab*, struct line*);
+static int		 wrap_page(struct tab*);
 static void		 redraw_tab(struct tab*);
 
+typedef void (*interactivefn)(int);
+
+static void	cmd_previous_line(int);
+static void	cmd_next_line(int);
+static void	cmd_forward_char(int);
+static void	cmd_backward_char(int);
+static void	cmd_redraw(int);
+static void	cmd_scroll_down(int);
+static void	cmd_scroll_up(int);
+static void	cmd_kill_telescope(int);
+
+static void	cmd_unbound(int);
+
+struct ui_state {
+	int			curs_x;
+	int			curs_y;
+	int			line_off;
+
+	TAILQ_HEAD(, line)	head;
+};
+
+static int
+push_line(struct tab *tab, const struct line *l, const char *buf, size_t len)
+{
+	struct line *vl;
+
+	if ((vl = calloc(1, sizeof(*vl))) == NULL)
+		return 0;
+
+	if (len != 0 && (vl->line = calloc(1, len+1)) == NULL) {
+		free(vl);
+		return 0;
+	}
+
+	vl->type = l->type;
+	if (len != 0)
+		memcpy(vl->line, buf, len);
+	vl->alt = l->alt;
+
+	if (TAILQ_EMPTY(&tab->s->head))
+		TAILQ_INSERT_HEAD(&tab->s->head, vl, lines);
+	else
+		TAILQ_INSERT_TAIL(&tab->s->head, vl, lines);
+	return 1;
+}
+
+static void
+empty_vlist(struct tab *tab)
+{
+	struct line *l, *t;
+
+	TAILQ_FOREACH_SAFE(l, &tab->s->head, lines, t) {
+		TAILQ_REMOVE(&tab->s->head, l, lines);
+		free(l->line);
+		/* l->alt references the original line! */
+		free(l);
+	}
+}
+
+struct binding {
+	int		key;
+	interactivefn	fn;
+} bindings[] = {
+	{ CTRL('p'),	cmd_previous_line, },
+	{ CTRL('n'),	cmd_next_line, },
+	{ CTRL('f'),	cmd_forward_char, },
+	{ CTRL('b'),	cmd_backward_char, },
+
+	{ CTRL('L'),	cmd_redraw, },
+
+	{ 'J',		cmd_scroll_down, },
+	{ 'K',		cmd_scroll_up, },
+
+	{ 'q',		cmd_kill_telescope, },
+
+	{ 0,		NULL, },
+};
+
+static void
+cmd_previous_line(int k)
+{
+	struct tab	*tab;
+
+	tab = current_tab();
+	tab->s->curs_y = MAX(0, tab->s->curs_y-1);
+	move(tab->s->curs_y, tab->s->curs_x);
+}
+
+static void
+cmd_next_line(int k)
+{
+	struct tab	*tab;
+
+	tab = current_tab();
+	tab->s->curs_y = MIN(LINES, tab->s->curs_y-1);
+	move(tab->s->curs_y, tab->s->curs_x);
+}
+
+static void
+cmd_forward_char(int k)
+{
+}
+
+static void
+cmd_backward_char(int k)
+{
+}
+
+static void
+cmd_redraw(int k)
+{
+	clear();
+	redraw_tab(current_tab());
+}
+
+static void
+cmd_scroll_down(int k)
+{
+	wscrl(stdscr, -1);
+}
+
+static void
+cmd_scroll_up(int k)
+{
+	wscrl(stdscr, 1);
+}
+
+static void
+cmd_kill_telescope(int k)
+{
+	event_loopbreak();
+}
+
+static void
+cmd_unbound(int k)
+{
+	/* TODO: flash a message */
+}
+
 static struct tab *
 current_tab(void)
 {
@@ -53,36 +241,40 @@ current_tab(void)
 static void
 dispatch_stdio(int fd, short ev, void *d)
 {
-	int c;
+	struct binding	*b;
+	int		 k;
 
-	c = getch();
+	k = getch();
 
-	if (c == ERR)
+	if (k == ERR)
 		return;
 
-	if (c == 'q') {
-		event_loopbreak();
-		return;
-	}
-
-	if (c == CTRL('L')) {
-		clear();
-		redraw_tab(current_tab());
-		return;
+	for (b = bindings; b->fn != NULL; ++b) {
+		if (k == b->key) {
+			b->fn(k);
+			goto done;
+		}
 	}
 
-	printw("You typed %c\n", c);
+	cmd_unbound(k);
+
+done:
 	refresh();
 }
 
 static void
 handle_resize(int sig, short ev, void *d)
 {
+	struct tab	*tab;
+
 	endwin();
 	refresh();
 	clear();
 
-	redraw_tab(current_tab());
+	tab = current_tab();
+
+	wrap_page(tab);
+	redraw_tab(tab);
 }
 
 /*
@@ -117,11 +309,15 @@ word_boundaries(const char *s, const char *sep, const 
 	return 1;
 }
 
-static inline void
-emitline(const char *prfx, size_t zero, size_t *off)
+static inline int
+emitline(struct tab *tab, size_t zero, size_t *off, const struct line *l,
+    const char **line)
 {
-	printw("\n%s", prfx);
+	if (!push_line(tab, l, *line, *off - zero))
+		return 0;
+	*line += *off - zero;
 	*off = zero;
+	return 1;
 }
 
 static inline void
@@ -137,100 +333,166 @@ emitstr(const char **s, size_t len, size_t *off)
 }
 
 /*
- * Wrap the text, prefixing the first line with prfx1 and the
- * following with prfx2, and breaking on characters in the breakon set.
- * The idea is pretty simple: if there is enough space, write the next
- * word; if we are at the start of a line and there's not enough
- * space, hard-split it.
+ * Build a list of visual line by wrapping the given line, assuming
+ * that when printed will have a leading prefix prfx.
  *
  * TODO: it considers each byte one cell on the screen!
- * TODO: assume strlen(prfx1) == strlen(prfx2)
  */
 static void
-wrap_text(const char *prfx1, const char *prfx2, const char *line, const char *breakon)
+wrap_text(struct tab *tab, const char *prfx, struct line *l)
 {
 	size_t		 zero, off, len, split;
-	const char	*endword, *endspc;
+	const char	*endword, *endspc, *line, *linestart;
 
-	printw("%s", prfx1);
-	zero = strlen(prfx1);
+	zero = strlen(prfx);
 	off = zero;
+	line = l->line;
+	linestart = l->line;
 
-	while (word_boundaries(line, breakon, &endword, &endspc)) {
+	while (word_boundaries(line, " \t-", &endword, &endspc)) {
 		len = endword - line;
-		if (off + len < COLS) {
-			emitstr(&line, len, &off);
-		} else {
-			emitline(prfx2, zero, &off);
+		if (off + len >= COLS) {
+			emitline(tab, zero, &off, l, &linestart);
 			while (len >= COLS) {
 				/* hard wrap */
-				printw("%*s", COLS-1, line);
-				emitline(prfx2, zero, &off);
+				emitline(tab, zero, &off, l, &linestart);
 				len -= COLS-1;
 				line += COLS-1;
 			}
 
 			if (len != 0)
-				emitstr(&line, len, &off);
-		}
+				off += len;
+		} else
+			off += len;
 
 		/* print the spaces iff not at bol */
 		len = endspc - endword;
 		/* line = endspc; */
 		if (off != zero) {
-			if (off + len >= COLS)
-				emitline(prfx2, zero, &off);
-			else
-				emitstr(&line, len, &off);
+			if (off + len >= COLS) {
+				emitline(tab, zero, &off, l, &linestart);
+				linestart = endspc;
+			} else
+				off += len;
 		}
 
 		line = endspc;
 	}
 
-	printw("\n");
+	emitline(tab, zero, &off, l, &linestart);
 }
 
-static void
-redraw_tab(struct tab *tab)
+static int
+hardwrap_text(struct tab *tab, struct line *l)
 {
-	struct line	*l;
-	const char	*sep = " \t";
+	size_t		 off, len;
+	const char	*linestart;
 
-	erase();
+        len = strlen(l->line);
+	off = 0;
+	linestart = l->line;
+
+	while (len >= COLS) {
+		len -= COLS-1;
+		off = COLS-1;
+		if (!emitline(tab, 0, &off, l, &linestart))
+			return 0;
+	}
+
+	return 1;
+}
 
+static int
+wrap_page(struct tab *tab)
+{
+	struct line	*l;
+
+	empty_vlist(tab);
+
 	TAILQ_FOREACH(l, &tab->page.head, lines) {
 		switch (l->type) {
 		case LINE_TEXT:
-			wrap_text("", "", l->line, sep);
+			wrap_text(tab, "", l);
 			break;
 		case LINE_LINK:
-			wrap_text("=> ", "   ", l->line, sep);
+			wrap_text(tab, "=> ", l);
 			break;
 		case LINE_TITLE_1:
-			wrap_text("# ", "  ", l->line, sep);
+			wrap_text(tab, "# ", l);
 			break;
 		case LINE_TITLE_2:
-			wrap_text("## ", "   ", l->line, sep);
+			wrap_text(tab, "## ", l);
 			break;
 		case LINE_TITLE_3:
-			wrap_text("### ", "    ", l->line, sep);
+			wrap_text(tab, "### ", l);
 			break;
 		case LINE_ITEM:
-			wrap_text("* ", "  ", l->line, sep);
+			wrap_text(tab, "* ", l);
 			break;
 		case LINE_QUOTE:
-			wrap_text("> ", "> ", l->line, sep);
+			wrap_text(tab, "> ", l);
 			break;
 		case LINE_PRE_START:
 		case LINE_PRE_END:
-			printw("```\n");
+                        push_line(tab, l, NULL, 0);
 			break;
 		case LINE_PRE_CONTENT:
-			printw("%s\n", l->line);
+                        hardwrap_text(tab, l);
 			break;
 		}
 	}
+	return 1;
+}
 
+static inline void
+print_line(struct line *l)
+{
+	switch (l->type) {
+	case LINE_TEXT:
+		break;
+	case LINE_LINK:
+		printw("=> ");
+		break;
+	case LINE_TITLE_1:
+		printw("# ");
+		break;
+	case LINE_TITLE_2:
+		printw("## ");
+		break;
+	case LINE_TITLE_3:
+		printw("### ");
+		break;
+	case LINE_ITEM:
+		printw("* ");
+		break;
+	case LINE_QUOTE:
+		printw("> ");
+		break;
+	case LINE_PRE_START:
+	case LINE_PRE_END:
+		printw("```");
+		break;
+	case LINE_PRE_CONTENT:
+		break;
+	}
+
+        if (l->line != NULL)
+		printw("%s", l->line);
+	printw("\n");
+}
+
+static void
+redraw_tab(struct tab *tab)
+{
+	struct line	*l;
+
+	erase();
+
+	TAILQ_FOREACH(l, &tab->s->head, lines) {
+		print_line(l);
+	}
+
+	move(tab->s->curs_y, tab->s->curs_x);
 	refresh();
 }
 
@@ -247,6 +509,8 @@ ui_init(void)
 	intrflush(stdscr, FALSE);
 	keypad(stdscr, TRUE);
 
+	scrollok(stdscr, TRUE);
+
 	/* non-blocking input */
 	timeout(0);
 
@@ -261,18 +525,27 @@ ui_init(void)
 	return 1;
 }
 
-void
+int
 ui_on_new_tab(struct tab *tab)
 {
 	struct tab	*t;
 
+	if ((tab->s = calloc(1, sizeof(*t->s))) == NULL)
+		return 0;
+
+	TAILQ_INIT(&tab->s->head);
+
 	TAILQ_FOREACH(t, &tabshead, tabs) {
 		t->flags &= ~TAB_CURRENT;
 	}
-
 	tab->flags = TAB_CURRENT;
 
 	/* TODO: redraw the tab list */
+	/* TODO: switch to the new tab */
+
+	move(0, 0);
+
+	return 1;
 }
 
 void
@@ -281,6 +554,7 @@ ui_on_tab_refresh(struct tab *tab)
 	if (!(tab->flags & TAB_CURRENT))
 		return;
 
+	wrap_page(tab);
 	redraw_tab(tab);
 }