commit 1d08c280e4fb59f78de88a09aa7932e3013e1ce7 from: Omar Polo date: Sat Mar 06 14:30:15 2021 UTC sync 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 #include @@ -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); }