From 7313cb05951e83b312fd1e2ae865e9579c2bbc0c Mon Sep 17 00:00:00 2001 From: Gregory Anders Date: Mon, 21 Dec 2020 19:13:02 -0700 Subject: [PATCH 1/5] Enable multi-select Default multi-select key is Ctrl+s --- fzy.1 | 8 ++++++-- src/choices.c | 39 ++++++++++++++++++++++++++++++++++++++- src/choices.h | 7 +++++++ src/tty.c | 6 +++++- src/tty.h | 1 + src/tty_interface.c | 37 +++++++++++++++++++++++++++++++------ test/test_choices.c | 26 ++++++++++++++++++++++++++ 7 files changed, 114 insertions(+), 10 deletions(-) diff --git a/fzy.1 b/fzy.1 index 79dbad7..96250eb 100644 --- a/fzy.1 +++ b/fzy.1 @@ -52,7 +52,7 @@ Usage help. . .TP .BR "ENTER" -Print the selected item to stdout and exit +Print the selected item (or all multi-selected items) to stdout and exit .TP .BR "Ctrl+c, Ctrl+g, Esc" Exit with status 1, without making a selection. @@ -63,7 +63,7 @@ Select the previous item .BR "Down Arrow, Ctrl+n" Select the next item .TP -Tab +.BR Tab Replace the current search string with the selected item .TP .BR "Backspace, Ctrl+h" @@ -74,6 +74,10 @@ Delete the word before the cursor .TP .BR Ctrl+u Delete the entire line +.TP +.BR Ctrl+s +Multi-select the current item. Each multi-selected item will be printed on +its own line. . .SH USAGE EXAMPLES . diff --git a/src/choices.c b/src/choices.c index fe2f80b..1244d06 100644 --- a/src/choices.c +++ b/src/choices.c @@ -94,13 +94,15 @@ static void choices_resize(choices_t *c, size_t new_capacity) { static void choices_reset_search(choices_t *c) { free(c->results); - c->selection = c->available = 0; c->results = NULL; + c->selection = c->available = 0; } void choices_init(choices_t *c, options_t *options) { c->strings = NULL; c->results = NULL; + c->selections = NULL; + c->num_selections = 0; c->buffer_size = 0; c->buffer = NULL; @@ -129,6 +131,10 @@ void choices_destroy(choices_t *c) { free(c->results); c->results = NULL; c->available = c->selection = 0; + + free(c->selections); + c->selections = NULL; + c->num_selections = 0; } void choices_add(choices_t *c, const char *choice) { @@ -141,6 +147,37 @@ void choices_add(choices_t *c, const char *choice) { c->strings[c->size++] = choice; } +void choices_select(choices_t *c, const char *choice) { + if (!c->selections) { + c->num_selections = 1; + c->selections = malloc(sizeof(char *)); + c->selections[0] = choice; + } else if (!choices_selected(c, choice)) { + c->num_selections++; + c->selections = realloc(c->selections, c->num_selections * sizeof(char *)); + c->selections[c->num_selections - 1] = choice; + } +} + +void choices_deselect(choices_t *c, const char *choice) { + for (size_t i = 0; i < c->num_selections; i++) { + if (c->selections[i] == choice) { + c->selections[i] = NULL; + c->num_selections--; + break; + } + } +} + +bool choices_selected(choices_t *c, const char *choice) { + for (size_t i = 0; i < c->num_selections; i++) { + if (c->selections[i] == choice) { + return true; + } + } + return false; +} + size_t choices_available(choices_t *c) { return c->available; } diff --git a/src/choices.h b/src/choices.h index 925478e..222bdb5 100644 --- a/src/choices.h +++ b/src/choices.h @@ -1,6 +1,7 @@ #ifndef CHOICES_H #define CHOICES_H CHOICES_H +#include #include #include "match.h" @@ -24,6 +25,9 @@ typedef struct { size_t available; size_t selection; + const char **selections; + size_t num_selections; + unsigned int worker_count; } choices_t; @@ -31,6 +35,9 @@ void choices_init(choices_t *c, options_t *options); void choices_fread(choices_t *c, FILE *file, char input_delimiter); void choices_destroy(choices_t *c); void choices_add(choices_t *c, const char *choice); +void choices_select(choices_t *c, const char *choice); +void choices_deselect(choices_t *c, const char *choice); +bool choices_selected(choices_t *c, const char *choice); size_t choices_available(choices_t *c); void choices_search(choices_t *c, const char *search); const char *choices_get(choices_t *c, size_t n); diff --git a/src/tty.c b/src/tty.c index 733477e..1f66ec8 100644 --- a/src/tty.c +++ b/src/tty.c @@ -148,9 +148,13 @@ void tty_setunderline(tty_t *tty) { tty_sgr(tty, 4); } +void tty_setbold(tty_t *tty) { + tty_sgr(tty, 1); +} + void tty_setnormal(tty_t *tty) { tty_sgr(tty, 0); - tty->fgcolor = 9; + tty->fgcolor = TTY_COLOR_NORMAL; } void tty_setnowrap(tty_t *tty) { diff --git a/src/tty.h b/src/tty.h index 013360e..f19d52f 100644 --- a/src/tty.h +++ b/src/tty.h @@ -22,6 +22,7 @@ int tty_input_ready(tty_t *tty, long int timeout, int return_on_signal); void tty_setfg(tty_t *tty, int fg); void tty_setinvert(tty_t *tty); void tty_setunderline(tty_t *tty); +void tty_setbold(tty_t *tty); void tty_setnormal(tty_t *tty); void tty_setnowrap(tty_t *tty); void tty_setwrap(tty_t *tty); diff --git a/src/tty_interface.c b/src/tty_interface.c index 343dde8..1839857 100644 --- a/src/tty_interface.c +++ b/src/tty_interface.c @@ -50,6 +50,10 @@ static void draw_match(tty_interface_t *state, const char *choice, int selected) } } + if (choices_selected(state->choices, choice)) { + tty_setbold(tty); + } + if (selected) #ifdef TTY_SELECTION_UNDERLINE tty_setunderline(tty); @@ -131,6 +135,17 @@ static void update_state(tty_interface_t *state) { } } +static void action_select(tty_interface_t *state) { + update_state(state); + + const char *selection = choices_get(state->choices, state->choices->selection); + if (choices_selected(state->choices, selection)) { + choices_deselect(state->choices, selection); + } else { + choices_select(state->choices, selection); + } +} + static void action_emit(tty_interface_t *state) { update_state(state); @@ -140,13 +155,21 @@ static void action_emit(tty_interface_t *state) { /* ttyout should be flushed before outputting on stdout */ tty_close(state->tty); - const char *selection = choices_get(state->choices, state->choices->selection); - if (selection) { - /* output the selected result */ - printf("%s\n", selection); + /* If no choices were selected with multi-select, use the choice under + * the cursor */ + if (!state->choices->num_selections) { + const char *selection = choices_get(state->choices, state->choices->selection); + if (selection) { + /* output the result */ + printf("%s\n", selection); + } else { + /* No match, output the query instead */ + printf("%s\n", state->search); + } } else { - /* No match, output the query instead */ - printf("%s\n", state->search); + for (size_t i = 0; i < state->choices->num_selections; i++) { + printf("%s\n", state->choices->selections[i]); + } } state->exit = EXIT_SUCCESS; @@ -265,6 +288,7 @@ static void append_search(tty_interface_t *state, char ch) { void tty_interface_init(tty_interface_t *state, tty_t *tty, choices_t *choices, options_t *options) { state->tty = tty; state->choices = choices; + state->options = options; state->ambiguous_key_pending = 0; @@ -300,6 +324,7 @@ static const keybinding_t keybindings[] = {{"\x1b", action_exit}, /* ESC * {KEY_CTRL('D'), action_exit}, /* C-D */ {KEY_CTRL('G'), action_exit}, /* C-G */ {KEY_CTRL('M'), action_emit}, /* CR */ + {KEY_CTRL('S'), action_select}, /* C-S */ {KEY_CTRL('P'), action_prev}, /* C-P */ {KEY_CTRL('N'), action_next}, /* C-N */ {KEY_CTRL('K'), action_prev}, /* C-K */ diff --git a/test/test_choices.c b/test/test_choices.c index d86bc12..86b764b 100644 --- a/test/test_choices.c +++ b/test/test_choices.c @@ -29,6 +29,7 @@ TEST test_choices_empty() { ASSERT_SIZE_T_EQ(0, choices.size); ASSERT_SIZE_T_EQ(0, choices.available); ASSERT_SIZE_T_EQ(0, choices.selection); + ASSERT_SIZE_T_EQ(0, choices.num_selections); choices_prev(&choices); ASSERT_SIZE_T_EQ(0, choices.selection); @@ -157,6 +158,30 @@ TEST test_choices_large_input() { PASS(); } +TEST test_choices_multi_select() { + choices_add(&choices, "tags"); + choices_add(&choices, "test"); + choices_search(&choices, ""); + + const char *first_choice = choices_get(&choices, 0); + ASSERT_FALSE(choices_selected(&choices, first_choice)); + choices_select(&choices, first_choice); + ASSERT_FALSE(!choices_selected(&choices, first_choice)); + ASSERT_SIZE_T_EQ(1, choices.num_selections); + + const char *second_choice = choices_get(&choices, 1); + ASSERT_FALSE(choices_selected(&choices, second_choice)); + choices_select(&choices, second_choice); + ASSERT_FALSE(!choices_selected(&choices, second_choice)); + ASSERT_SIZE_T_EQ(2, choices.num_selections); + + choices_deselect(&choices, second_choice); + ASSERT_FALSE(choices_selected(&choices, second_choice)); + ASSERT_SIZE_T_EQ(1, choices.num_selections); + + PASS(); +} + SUITE(choices_suite) { SET_SETUP(setup, NULL); SET_TEARDOWN(teardown, NULL); @@ -167,4 +192,5 @@ SUITE(choices_suite) { RUN_TEST(test_choices_without_search); RUN_TEST(test_choices_unicode); RUN_TEST(test_choices_large_input); + RUN_TEST(test_choices_multi_select); } From f37b488824117663f1326c399d61923ed785fa2c Mon Sep 17 00:00:00 2001 From: Gregory Anders Date: Mon, 21 Dec 2020 19:13:02 -0700 Subject: [PATCH 2/5] Clean-up multiselect code Allocate memory for the multiselect buffer in chunks, similar to how the `choices` buffer is resized. This results in potentially more unused (i.e. wasted) memory, but in fewer invocations of `malloc`/`realloc`, which is probably more important, considering the size of the multiselect array will never be that large. --- src/choices.c | 53 +++++++++++++++++++++++++++++---------------- src/choices.h | 8 +++++-- src/tty_interface.c | 6 ++--- test/test_choices.c | 23 ++++++++++---------- 4 files changed, 55 insertions(+), 35 deletions(-) diff --git a/src/choices.c b/src/choices.c index 1244d06..09733d6 100644 --- a/src/choices.c +++ b/src/choices.c @@ -15,6 +15,9 @@ /* Initial size of choices array */ #define INITIAL_CHOICE_CAPACITY 128 +/* Initial size of multi-selection buffer */ +#define INITIAL_SELECTIONS_CAPACITY 8 + static int cmpchoice(const void *_idx1, const void *_idx2) { const struct scored_result *a = _idx1; const struct scored_result *b = _idx2; @@ -92,6 +95,11 @@ static void choices_resize(choices_t *c, size_t new_capacity) { c->capacity = new_capacity; } +static void choices_resize_selections(choices_t *c, size_t new_capacity) { + c->selections.strings = safe_realloc(c->selections.strings, new_capacity * sizeof(const char *)); + c->selections.capacity = new_capacity; +} + static void choices_reset_search(choices_t *c) { free(c->results); c->results = NULL; @@ -101,8 +109,8 @@ static void choices_reset_search(choices_t *c) { void choices_init(choices_t *c, options_t *options) { c->strings = NULL; c->results = NULL; - c->selections = NULL; - c->num_selections = 0; + c->selections.strings = NULL; + c->selections.capacity = c->selections.size = 0; c->buffer_size = 0; c->buffer = NULL; @@ -132,9 +140,9 @@ void choices_destroy(choices_t *c) { c->results = NULL; c->available = c->selection = 0; - free(c->selections); - c->selections = NULL; - c->num_selections = 0; + free(c->selections.strings); + c->selections.strings = NULL; + c->selections.capacity = c->selections.size = 0; } void choices_add(choices_t *c, const char *choice) { @@ -148,30 +156,37 @@ void choices_add(choices_t *c, const char *choice) { } void choices_select(choices_t *c, const char *choice) { - if (!c->selections) { - c->num_selections = 1; - c->selections = malloc(sizeof(char *)); - c->selections[0] = choice; - } else if (!choices_selected(c, choice)) { - c->num_selections++; - c->selections = realloc(c->selections, c->num_selections * sizeof(char *)); - c->selections[c->num_selections - 1] = choice; + if (c->selections.size == c->selections.capacity) { + if (c->selections.capacity == 0) { + choices_resize_selections(c, INITIAL_SELECTIONS_CAPACITY); + } else { + choices_resize_selections(c, c->selections.capacity * 2); + } + } + + if (!choices_selected(c, choice)) { + c->selections.strings[c->selections.size++] = choice; } } void choices_deselect(choices_t *c, const char *choice) { - for (size_t i = 0; i < c->num_selections; i++) { - if (c->selections[i] == choice) { - c->selections[i] = NULL; - c->num_selections--; + size_t index = c->selections.size; + for (size_t i = 0; i < c->selections.size; i++) { + if (c->selections.strings[i] == choice) { + c->selections.size--; + index = i; break; } } + + for (size_t i = index; i < c->selections.size; i++) { + c->selections.strings[i] = c->selections.strings[i+1]; + } } bool choices_selected(choices_t *c, const char *choice) { - for (size_t i = 0; i < c->num_selections; i++) { - if (c->selections[i] == choice) { + for (size_t i = 0; i < c->selections.size; i++) { + if (c->selections.strings[i] == choice) { return true; } } diff --git a/src/choices.h b/src/choices.h index 222bdb5..50ef7dc 100644 --- a/src/choices.h +++ b/src/choices.h @@ -25,8 +25,12 @@ typedef struct { size_t available; size_t selection; - const char **selections; - size_t num_selections; + struct { + const char **strings; + + size_t capacity; + size_t size; + } selections; unsigned int worker_count; } choices_t; diff --git a/src/tty_interface.c b/src/tty_interface.c index 1839857..07fbeda 100644 --- a/src/tty_interface.c +++ b/src/tty_interface.c @@ -157,7 +157,7 @@ static void action_emit(tty_interface_t *state) { /* If no choices were selected with multi-select, use the choice under * the cursor */ - if (!state->choices->num_selections) { + if (!state->choices->selections.size) { const char *selection = choices_get(state->choices, state->choices->selection); if (selection) { /* output the result */ @@ -167,8 +167,8 @@ static void action_emit(tty_interface_t *state) { printf("%s\n", state->search); } } else { - for (size_t i = 0; i < state->choices->num_selections; i++) { - printf("%s\n", state->choices->selections[i]); + for (size_t i = 0; i < state->choices->selections.size; i++) { + printf("%s\n", state->choices->selections.strings[i]); } } diff --git a/test/test_choices.c b/test/test_choices.c index 86b764b..858e88f 100644 --- a/test/test_choices.c +++ b/test/test_choices.c @@ -9,27 +9,28 @@ #include "greatest/greatest.h" #define ASSERT_SIZE_T_EQ(a,b) ASSERT_EQ_FMT((size_t)(a), (b), "%zu") +#define ASSERT_TRUE(cond) ASSERT_FALSE(!cond) static options_t default_options; static choices_t choices; static void setup(void *udata) { - (void)udata; + (void)udata; - options_init(&default_options); - choices_init(&choices, &default_options); + options_init(&default_options); + choices_init(&choices, &default_options); } static void teardown(void *udata) { - (void)udata; - choices_destroy(&choices); + (void)udata; + choices_destroy(&choices); } TEST test_choices_empty() { ASSERT_SIZE_T_EQ(0, choices.size); ASSERT_SIZE_T_EQ(0, choices.available); ASSERT_SIZE_T_EQ(0, choices.selection); - ASSERT_SIZE_T_EQ(0, choices.num_selections); + ASSERT_SIZE_T_EQ(0, choices.selections.size); choices_prev(&choices); ASSERT_SIZE_T_EQ(0, choices.selection); @@ -166,18 +167,18 @@ TEST test_choices_multi_select() { const char *first_choice = choices_get(&choices, 0); ASSERT_FALSE(choices_selected(&choices, first_choice)); choices_select(&choices, first_choice); - ASSERT_FALSE(!choices_selected(&choices, first_choice)); - ASSERT_SIZE_T_EQ(1, choices.num_selections); + ASSERT_TRUE(choices_selected(&choices, first_choice)); + ASSERT_SIZE_T_EQ(1, choices.selections.size); const char *second_choice = choices_get(&choices, 1); ASSERT_FALSE(choices_selected(&choices, second_choice)); choices_select(&choices, second_choice); - ASSERT_FALSE(!choices_selected(&choices, second_choice)); - ASSERT_SIZE_T_EQ(2, choices.num_selections); + ASSERT_TRUE(choices_selected(&choices, second_choice)); + ASSERT_SIZE_T_EQ(2, choices.selections.size); choices_deselect(&choices, second_choice); ASSERT_FALSE(choices_selected(&choices, second_choice)); - ASSERT_SIZE_T_EQ(1, choices.num_selections); + ASSERT_SIZE_T_EQ(1, choices.selections.size); PASS(); } From 6027a95095fa3c1d8158131c8a216dbd26313995 Mon Sep 17 00:00:00 2001 From: Gregory Anders Date: Mon, 21 Dec 2020 19:13:02 -0700 Subject: [PATCH 3/5] Only add non-null selections --- src/tty_interface.c | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/tty_interface.c b/src/tty_interface.c index 07fbeda..c9fdcc7 100644 --- a/src/tty_interface.c +++ b/src/tty_interface.c @@ -139,10 +139,12 @@ static void action_select(tty_interface_t *state) { update_state(state); const char *selection = choices_get(state->choices, state->choices->selection); - if (choices_selected(state->choices, selection)) { - choices_deselect(state->choices, selection); - } else { - choices_select(state->choices, selection); + if (selection) { + if (choices_selected(state->choices, selection)) { + choices_deselect(state->choices, selection); + } else { + choices_select(state->choices, selection); + } } } From 7e8bc19a45ac8821051a01a39a1bf6859f8e45e8 Mon Sep 17 00:00:00 2001 From: Gregory Anders Date: Mon, 21 Dec 2020 19:13:02 -0700 Subject: [PATCH 4/5] Move cursor to next choice after multi-(de)selecting --- src/tty_interface.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/tty_interface.c b/src/tty_interface.c index c9fdcc7..381d46d 100644 --- a/src/tty_interface.c +++ b/src/tty_interface.c @@ -145,6 +145,7 @@ static void action_select(tty_interface_t *state) { } else { choices_select(state->choices, selection); } + choices_next(state->choices); } } From fca09040b7f57d8f5a7e89654df7695968e52299 Mon Sep 17 00:00:00 2001 From: Gregory Anders Date: Mon, 21 Dec 2020 19:13:02 -0700 Subject: [PATCH 5/5] Change multi-select key to Ctrl+t (mnemonic: "tag") --- fzy.1 | 2 +- src/tty_interface.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fzy.1 b/fzy.1 index 96250eb..0a181b9 100644 --- a/fzy.1 +++ b/fzy.1 @@ -75,7 +75,7 @@ Delete the word before the cursor .BR Ctrl+u Delete the entire line .TP -.BR Ctrl+s +.BR Ctrl+t Multi-select the current item. Each multi-selected item will be printed on its own line. . diff --git a/src/tty_interface.c b/src/tty_interface.c index 381d46d..25e4b16 100644 --- a/src/tty_interface.c +++ b/src/tty_interface.c @@ -327,7 +327,7 @@ static const keybinding_t keybindings[] = {{"\x1b", action_exit}, /* ESC * {KEY_CTRL('D'), action_exit}, /* C-D */ {KEY_CTRL('G'), action_exit}, /* C-G */ {KEY_CTRL('M'), action_emit}, /* CR */ - {KEY_CTRL('S'), action_select}, /* C-S */ + {KEY_CTRL('T'), action_select}, /* C-T */ {KEY_CTRL('P'), action_prev}, /* C-P */ {KEY_CTRL('N'), action_next}, /* C-N */ {KEY_CTRL('K'), action_prev}, /* C-K */