diff --git a/Makefile.am b/Makefile.am index 803d026a..f6ccb444 100644 --- a/Makefile.am +++ b/Makefile.am @@ -4,7 +4,11 @@ AM_CFLAGS=-Wall -Wextra AM_CPPFLAGS=-D_GNU_SOURCE bin_PROGRAMS=pick -pick_SOURCES=pick.c compat-reallocarray.c compat-strtonum.c compat.h +pick_SOURCES=compat-reallocarray.c \ + compat-strtonum.c \ + compat-xpoll.c \ + compat.h \ + pick.c pick_CPPFLAGS=$(AM_CPPFLAGS) $(NCURSES_CFLAGS) pick_LDADD=$(NCURSES_LIBS) diff --git a/compat-xpoll.c b/compat-xpoll.c new file mode 100644 index 00000000..19571e1e --- /dev/null +++ b/compat-xpoll.c @@ -0,0 +1,121 @@ +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include + +#include "compat.h" + +#ifdef HAVE_BROKEN_POLL + +#include +#include +#include +#include + +static int tty_peekc(int); + +int +xpoll(struct pollfd *fds, nfds_t nfds, int timeout) +{ + static int state; + int flags, inready, ttyready; + int nready = 0; + + /* XXX filter_choices() poll disabled */ + if (nfds == 1 && timeout == 0) + return 0; + + if (state == 0) { + assert(nfds == 2); + + flags = fcntl(fds[0].fd, F_GETFL, 0); + if (flags == -1) + return -1; + flags |= O_NONBLOCK; + if (fcntl(fds[0].fd, F_SETFL, flags) == -1) + return -1; + state++; + } + + if (state == 1) { + if (timeout < 0) + timeout = 100; + while (nready == 0) { + if (nfds == 1) { + state++; + break; + } + + inready = poll(&fds[1], 1, timeout); + if (inready == -1) + return -1; + nready += inready; + + ttyready = tty_peekc(fds[0].fd); + if (ttyready == -1) { + return -1; + } else if (ttyready > 0) { + fds[0].revents = POLLIN; + nready++; + } else { + fds[0].revents = 0; + } + } + } + + if (state == 2) { + flags = fcntl(fds[0].fd, F_GETFL, 0); + if (flags == -1) + return -1; + flags &= ~O_NONBLOCK; + if (fcntl(fds[0].fd, F_SETFL, flags) == -1) + return -1; + state++; + } + + if (state == 3) { + ttyready = tty_peekc(fds[0].fd); + if (ttyready == -1) { + return -1; + } else if (ttyready > 0) { + fds[0].revents = POLLIN; + nready++; + } else { + fds[0].revents = 0; + } + } + + return nready; +} + +static int +tty_peekc(int fd) +{ + extern int tty_getc_peek; + ssize_t n; + unsigned char c; + + n = read(fd, &c, sizeof(c)); + if (n == -1) { + if (errno == EAGAIN) + return 0; + return -1; + } + if (n > 0) { + assert(tty_getc_peek == -1); + tty_getc_peek = c; + } + return n; +} + +#else + +int +xpoll(struct pollfd *fds, nfds_t nfds, int timeout) +{ + return poll(fds, nfds, timeout); +} + +#endif /* HAVE_BROKEN_POLL */ diff --git a/compat.h b/compat.h index f8bbc0ce..1f706948 100644 --- a/compat.h +++ b/compat.h @@ -32,3 +32,5 @@ long long strtonum(const char *, long long, long long, const char **); #endif /* !HAVE_STRTONUM */ #endif /* COMPAT_H */ + +int xpoll(struct pollfd *fds, nfds_t nfds, int timeout); diff --git a/config.h.in b/config.h.in index 66b3dbd6..246da3fa 100644 --- a/config.h.in +++ b/config.h.in @@ -1,5 +1,8 @@ /* config.h.in. Generated from configure.ac by autoheader. */ +/* Define if poll does not support character devices */ +#undef HAVE_BROKEN_POLL + /* Define if ncursesw is available */ #undef HAVE_NCURSESW_H diff --git a/configure.ac b/configure.ac index dfa1fa12..84b292de 100644 --- a/configure.ac +++ b/configure.ac @@ -2,6 +2,7 @@ AC_PREREQ([2.61]) AC_INIT([pick], [2.0.2], [pick-maintainers@calleerlandsson.com]) AM_INIT_AUTOMAKE([subdir-objects]) AC_CONFIG_HEADERS([config.h]) +AC_CANONICAL_HOST AC_PROG_CC AM_PROG_CC_C_O AC_CHECK_FUNCS([pledge reallocarray strtonum]) @@ -16,7 +17,6 @@ AC_SEARCH_LIBS([setupterm], [curses], [], [ ) ]) AC_DEFUN([AC_MALLOC_OPTIONS], [ - AC_CANONICAL_HOST AC_MSG_CHECKING([for $host_os malloc hardening options]) case "$host_os" in openbsd*) malloc_options="RS";; @@ -30,5 +30,20 @@ AC_DEFUN([AC_MALLOC_OPTIONS], [ AC_SUBST([MALLOC_OPTIONS], [$malloc_options]) ]) AC_MALLOC_OPTIONS +AC_DEFUN([AC_BROKEN_POLL], [ + AC_MSG_CHECKING([for broken poll implementation]) + case "$host_os" in + darwin*) broken=1;; + *) broken=0;; + esac + if test "$broken" -eq 1; then + AC_MSG_RESULT([yes]) + AC_DEFINE([HAVE_BROKEN_POLL], [1], [ + Define if poll does not support character devices]) + else + AC_MSG_RESULT([no]) + fi +]) +AC_BROKEN_POLL AC_CONFIG_FILES([Makefile]) AC_OUTPUT diff --git a/pick.c b/pick.c index 4ba17de3..db4f407d 100644 --- a/pick.c +++ b/pick.c @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -60,19 +61,24 @@ enum key { }; struct choice { - const char *description; - const char *string; + char *const *string; + size_t offset; size_t length; + size_t description; ssize_t match_start; /* inclusive match start offset */ ssize_t match_end; /* exclusive match end offset */ double score; }; -static int choicecmp(const void *, const void *); +int tty_getc_peek = -1; + +static int choice_cmp(const void *, const void *); +static const char *choice_description(const struct choice *); +static const char *choice_string(const struct choice *); static void delete_between(char *, size_t, size_t, size_t); static char *eager_strpbrk(const char *, const char *); -static int filter_choices(size_t); -static char *get_choices(void); +static int filter_choices(size_t, size_t *); +static size_t get_choices(int, ssize_t); static enum key get_key(const char **); static void handle_sigwinch(int); static int isu8cont(unsigned char); @@ -80,7 +86,7 @@ static int isu8start(unsigned char); static int isword(const char *); static size_t min_match(const char *, size_t, ssize_t *, ssize_t *); -static size_t print_choices(size_t, size_t); +static void print_choices(size_t, size_t, size_t); static void print_line(const char *, size_t, int, ssize_t, ssize_t); static const struct choice *selected_choice(void); @@ -117,7 +123,6 @@ int main(int argc, char *argv[]) { const struct choice *choice; - char *input; int c; int output_description = 0; int rc = 0; @@ -178,7 +183,6 @@ main(int argc, char *argv[]) err(1, NULL); } - input = get_choices(); tty_init(1); #ifdef HAVE_PLEDGE @@ -189,14 +193,15 @@ main(int argc, char *argv[]) choice = selected_choice(); tty_restore(1); if (choice != NULL) { - printf("%s\n", choice->string); + printf("%s\n", choice_string(choice)); if (output_description) - printf("%s\n", choice->description); + printf("%s\n", choice_description(choice)); } else { rc = 1; } - free(input); + if (choices.length > 0) + free(*choices.v[0].string); free(choices.v); free(query); @@ -220,67 +225,87 @@ usage(int status) exit(status); } -char * -get_choices(void) +size_t +get_choices(int fd, ssize_t insert) { - char *buf, *description, *ifs, *start, *stop; - ssize_t n; - size_t length = 0; - size_t size = BUFSIZ; - - if ((ifs = getenv("IFS")) == NULL || *ifs == '\0') + static const char *ifs; + static char *buf; + static size_t length, offset; + static size_t size = BUFSIZ; + struct choice *new, *old, tmp; + char *desc, *start, *stop; + ssize_t n; + size_t dchoices, i, nchoices; + + if (ifs == NULL && ((ifs = getenv("IFS")) == NULL || *ifs == '\0')) ifs = " "; - if ((buf = malloc(size)) == NULL) - err(1, NULL); - for (;;) { - if ((n = read(STDIN_FILENO, buf + length, size - length)) == -1) - err(1, "read"); - else if (n == 0) - break; - - length += n; - if (length + 1 < size) - continue; - if ((buf = reallocarray(buf, 2, size)) == NULL) + if (buf == NULL || length + 1 == size) { + buf = reallocarray(buf, 2, size); + if (buf == NULL) err(1, NULL); size *= 2; } + + n = read(fd, buf + length, size - length - 1); + if (n == -1) + err(1, "read"); + length += n; buf[length] = '\0'; - choices.size = 16; - if ((choices.v = reallocarray(NULL, choices.size, - sizeof(struct choice))) == NULL) - err(1, NULL); + if (choices.v == NULL) { + choices.v = reallocarray(NULL, 16, sizeof(struct choice)); + if (choices.v == NULL) + err(1, NULL); + choices.size = 16; + } - start = buf; + nchoices = choices.length; + start = buf + offset; while ((stop = strchr(start, '\n')) != NULL) { *stop = '\0'; - if (descriptions && (description = eager_strpbrk(start, ifs))) - *description++ = '\0'; - else - description = ""; + /* Ensure room for a extra choice when ALT_ENTER is invoked. */ + if (choices.length + 2 >= choices.size) { + choices.v = reallocarray(choices.v, 2 * choices.size, + sizeof(struct choice)); + if (choices.v == NULL) + err(1, NULL); + choices.size *= 2; + } + choices.v[choices.length].string = &buf; + choices.v[choices.length].offset = start - buf; choices.v[choices.length].length = stop - start; - choices.v[choices.length].string = start; - choices.v[choices.length].description = description; choices.v[choices.length].match_start = -1; choices.v[choices.length].match_end = -1; choices.v[choices.length].score = 0; + if (descriptions && (desc = eager_strpbrk(start, ifs))) { + *desc++ = '\0'; + choices.v[choices.length].description = desc - buf; + } else { + choices.v[choices.length].description = -1; + } - start = stop + 1; + choices.length++; - /* Ensure room for a extra choice when ALT_ENTER is invoked. */ - if (++choices.length + 1 < choices.size) - continue; - choices.size *= 2; - if ((choices.v = reallocarray(choices.v, choices.size, - sizeof(struct choice))) == NULL) - err(1, NULL); + start = stop + 1; + } + offset = start - buf; + dchoices = choices.length - nchoices; + if (dchoices == 0 || nchoices == 0 || insert == -1) + return n; + + /* Move new choices after the given insert index. */ + for (i = 0; i < dchoices; i++) { + old = choices.v + insert + i; + new = choices.v + nchoices + i; + tmp = *new; + *new = *old; + *old = tmp; } - return buf; + return n; } char * @@ -300,59 +325,73 @@ eager_strpbrk(const char *string, const char *separators) const struct choice * selected_choice(void) { + struct pollfd fds[2]; const char *buf; - size_t cursor_position, i, j, length, xscroll; + ssize_t insert; + size_t choices_offset, cursor_position, i, j, length, nfds, + xscroll; size_t choices_count = 0; size_t selection = 0; size_t yscroll = 0; + int dokey, doread, nready, timo; + int choices_reset = 1; int dochoices = 0; int dofilter = 1; - int query_grew = 0; cursor_position = query_length; + fds[0].fd = fileno(tty_in); + fds[0].events = POLLIN; + fds[1].fd = STDIN_FILENO; + fds[1].events = POLLIN; + nfds = 2; + /* No timeout on first call to poll in order to render the UI fast. */ + timo = 0; for (;;) { - /* - * If the user didn't add more characters to the query all - * choices have to be reconsidered as potential matches. - * In the opposite scenario, there's no point in reconsidered - * all choices again since the ones that didn't match the - * previous query will clearly not match the current one due to - * the fact that previous query is a left-most substring of the - * current one. - */ - if (!query_grew) - choices_count = choices.length; - query_grew = 0; - if (dofilter) { - if ((dochoices = filter_choices(choices_count))) - dofilter = selection = yscroll = 0; + choices_offset = dokey = doread = 0; + toggle_sigwinch(1); + nready = xpoll(fds, nfds, timo); + if (nready == -1 && errno != EINTR) + err(1, "poll"); + toggle_sigwinch(0); + if (gotsigwinch) { + gotsigwinch = 0; + goto resize; + } + timo = -1; + for (i = 0; i < nfds; i++) { + if (fds[i].revents & (POLLERR | POLLNVAL)) { + errno = EBADF; + err(1, "poll"); + } + if ((fds[i].revents & (POLLIN | POLLHUP)) == 0) + continue; + + if (fds[i].fd == STDIN_FILENO) + doread = 1; + else if (fds[i].fd == fileno(tty_in)) + dokey = 1; } - tty_putp(cursor_invisible, 0); - tty_putp(carriage_return, 1); /* move cursor to first column */ - if (cursor_position >= tty_columns) - xscroll = cursor_position - tty_columns + 1; - else - xscroll = 0; - print_line(&query[xscroll], query_length - xscroll, 0, -1, -1); - if (dochoices) { - if (selection - yscroll >= choices_lines) - yscroll = selection - choices_lines + 1; - choices_count = print_choices(yscroll, selection); + length = choices.length; + if (doread) { + insert = query_length > 0 ? (ssize_t)choices_count : -1; + if (get_choices(fds[1].fd, insert)) { + if (query_length > 0) { + dofilter = 1; + choices_reset = 0; + choices_offset = choices_count; + choices_count += + choices.length - length; + } else { + choices_reset = 1; + } + } else { + nfds--; /* EOF */ + } } - tty_putp(carriage_return, 1); /* move cursor to first column */ - for (i = j = 0; i < cursor_position; j++) - while (isu8cont(query[++i])) - continue; - if (j > 0) - /* - * parm_right_cursor interprets 0 as 1, therefore only - * move the cursor if the position is non zero. - */ - tty_putp(tty_parm1(parm_right_cursor, j), 1); - tty_putp(cursor_normal, 0); - fflush(tty_out); + if (!dokey) + goto render; switch (get_key(&buf)) { case ENTER: @@ -360,8 +399,9 @@ selected_choice(void) return &choices.v[selection]; break; case ALT_ENTER: - choices.v[choices.length].string = query; - choices.v[choices.length].description = ""; + choices.v[choices.length].string = &query; + choices.v[choices.length].offset = 0; + choices.v[choices.length].description = -1; return &choices.v[choices.length]; case CTRL_C: return NULL; @@ -380,7 +420,7 @@ selected_choice(void) cursor_position - length, cursor_position); cursor_position -= length; query_length -= length; - dofilter = 1; + dofilter = choices_reset = 1; } break; case DEL: @@ -392,27 +432,28 @@ selected_choice(void) delete_between(query, query_length, cursor_position, cursor_position + length); query_length -= length; - dofilter = 1; + dofilter = choices_reset = 1; } break; case CTRL_U: delete_between(query, query_length, 0, cursor_position); query_length -= cursor_position; cursor_position = 0; - dofilter = 1; + dofilter = choices_reset = 1; break; case CTRL_K: delete_between(query, query_length, cursor_position, query_length); query_length = cursor_position; - dofilter = 1; + dofilter = choices_reset = 1; break; case CTRL_L: + resize: tty_size(); break; case CTRL_O: sort = !sort; - dofilter = 1; + dofilter = choices_reset = 1; break; case CTRL_W: if (cursor_position == 0) @@ -433,7 +474,7 @@ selected_choice(void) delete_between(query, query_length, i, cursor_position); query_length -= cursor_position - i; cursor_position = i; - dofilter = 1; + dofilter = choices_reset = 1; break; case CTRL_A: cursor_position = 0; @@ -485,8 +526,10 @@ selected_choice(void) yscroll = selection = 0; break; case PRINTABLE: - length = strlen(buf); + if (query_length == 0) + choices_reset = 1; + length = strlen(buf); if (query_length + length >= query_size) { query_size = 2*query_length + length; if ((query = reallocarray(query, query_size, @@ -503,31 +546,71 @@ selected_choice(void) cursor_position += length; query_length += length; query[query_length] = '\0'; - dofilter = query_grew = 1; + dofilter = 1; break; case UNKNOWN: break; } + +render: + if (choices_reset) + choices_count = choices.length; + choices_reset = 0; + if (dofilter) { + dochoices = filter_choices(choices_offset, + &choices_count); + if (dochoices) + dofilter = 0; + } + + tty_putp(cursor_invisible, 0); + tty_putp(carriage_return, 1); /* move cursor to first column */ + if (cursor_position >= tty_columns) + xscroll = cursor_position - tty_columns + 1; + else + xscroll = 0; + print_line(&query[xscroll], query_length - xscroll, 0, -1, -1); + if (dochoices) { + if (selection >= choices_count) + selection = yscroll = 0; + if (selection - yscroll >= choices_lines) + yscroll = selection - choices_lines + 1; + print_choices(yscroll, choices_count, selection); + } + tty_putp(carriage_return, 1); /* move cursor to first column */ + for (i = j = 0; i < cursor_position; j++) + while (isu8cont(query[++i])) + continue; + if (j > 0) + /* + * parm_right_cursor interprets 0 as 1, therefore only + * move the cursor if the position is non zero. + */ + tty_putp(tty_parm1(parm_right_cursor, j), 1); + tty_putp(cursor_normal, 0); + fflush(tty_out); } } /* - * Filter the first nchoices number of choices using the current query and - * regularly check for new user input in order to abort filtering. This - * improves the performance when the cardinality of the choices is large. + * Filter nchoices - offset number of choices starting at offset using the + * current query. + * In addition, regularly check for new user input and abort filtering since any + * previous matches could be invalidated by the new query. * Returns non-zero if the filtering was not aborted. */ int -filter_choices(size_t nchoices) +filter_choices(size_t offset, size_t *nchoices) { - struct choice *c; struct pollfd pfd; + struct choice *c; size_t i, match_length; + size_t n = 0; int nready; - for (i = 0; i < nchoices; i++) { + for (i = offset; i < *nchoices; i++) { c = &choices.v[i]; - if (min_match(c->string, 0, + if (min_match(choice_string(c), 0, &c->match_start, &c->match_end) == INT_MAX) { c->match_start = c->match_end = -1; c->score = 0; @@ -537,23 +620,26 @@ filter_choices(size_t nchoices) match_length = c->match_end - c->match_start; c->score = (double)query_length/match_length/c->length; } + if (c->score > 0 || query_length == 0) + n++; if (i > 0 && i % 50 == 0) { pfd.fd = fileno(tty_in); pfd.events = POLLIN; - if ((nready = poll(&pfd, 1, 0)) == -1) + if ((nready = xpoll(&pfd, 1, 0)) == -1) err(1, "poll"); if (nready == 1 && pfd.revents & (POLLIN | POLLHUP)) return 0; } } - qsort(choices.v, nchoices, sizeof(struct choice), choicecmp); + qsort(choices.v, *nchoices, sizeof(struct choice), choice_cmp); + *nchoices = offset + n; return 1; } int -choicecmp(const void *p1, const void *p2) +choice_cmp(const void *p1, const void *p2) { const struct choice *c1, *c2; @@ -565,18 +651,31 @@ choicecmp(const void *p1, const void *p2) return -1; /* * The two choices have an equal score. - * Sort based on the address of string since it reflects the initial - * input order. + * Sort based on the offset since it reflects the initial input order. * The comparison is inverted since the choice with the lowest address * must come first. */ - if (c1->string < c2->string) + if (c1->offset < c2->offset) return -1; - if (c1->string > c2->string) + if (c1->offset > c2->offset) return 1; return 0; } +const char * +choice_description(const struct choice *c) +{ + if (c->description == (size_t)-1) + return ""; + return *c->string + c->description; +} + +const char * +choice_string(const struct choice *c) +{ + return *c->string + c->offset; +} + size_t min_match(const char *string, size_t offset, ssize_t *start, ssize_t *end) { @@ -850,25 +949,21 @@ print_line(const char *str, size_t len, int standout, } /* - * Output as many choices as possible starting from offset and return the number - * of choices with a positive score. If the query is empty, all choices are - * considered having a positive score. + * Print length - offset number of choices. */ -size_t -print_choices(size_t offset, size_t selection) +void +print_choices(size_t offset, size_t length, size_t selection) { - const struct choice *choice; + const struct choice *c; size_t i; - for (i = offset; i < choices.length; i++) { - choice = choices.v + i; - if (choice->score == 0 && query_length > 0) + for (i = offset; i < length; i++) { + if (i - offset >= choices_lines) break; - if (i - offset < choices_lines) - print_line(choice->string, choice->length, - i == selection, choice->match_start, - choice->match_end); + c = choices.v + i; + print_line(choice_string(c), c->length, i == selection, + c->match_start, c->match_end); } if (i - offset < choices.length && i - offset < choices_lines) { @@ -894,8 +989,6 @@ print_choices(size_t offset, size_t selection) tty_putp(tty_parm1(parm_up_cursor, i < choices_lines ? i : choices_lines), 1); } - - return i; } enum key @@ -959,18 +1052,7 @@ get_key(const char **key) *key = (const char *)buf; len = 0; - /* - * Allow SIGWINCH on the first read. If the signal is received, return - * CTRL_L which will trigger a resize. - */ - toggle_sigwinch(1); buf[len++] = tty_getc(); - toggle_sigwinch(0); - if (gotsigwinch) { - gotsigwinch = 0; - return CTRL_L; - } - for (;;) { for (i = 0; keys[i].key != UNKNOWN; i++) { if (keys[i].tio >= 0) { @@ -1042,11 +1124,20 @@ get_key(const char **key) int tty_getc(void) { - int c; + ssize_t n; + unsigned char c; - if ((c = getc(tty_in)) == ERR && !gotsigwinch) - err(1, "getc"); + if (tty_getc_peek != -1) { + c = tty_getc_peek; + tty_getc_peek = -1; + return c; + } + n = read(fileno(tty_in), &c, sizeof(c)); + if (n == -1) + err(1, "read"); + if (n == 0) + return EOF; return c; } diff --git a/tests/key-printable.t b/tests/key-printable.t index b265396b..54e2751b 100644 --- a/tests/key-printable.t +++ b/tests/key-printable.t @@ -33,7 +33,7 @@ stdin: stdout: 💩 -description: changing the query resets vertical scroll +description: changing the query does not reset the selection if it is still in bounds keys: \016 \016 \016 \016 \016 0 \n #DOWN ENTER env: LINES=5 stdin: @@ -43,4 +43,16 @@ stdin: 04 05 stdout: +05 + +description: changing the query does reset the selection if it is out of bounds +keys: \016 \016 \016 \016 \016 1 \n #DOWN ENTER +env: LINES=5 +stdin: +01 +02 +03 +04 +05 +stdout: 01 diff --git a/tests/pick-test.c b/tests/pick-test.c index ab7cea7d..c17ec96f 100644 --- a/tests/pick-test.c +++ b/tests/pick-test.c @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include