fzy
terminal fuzzy finder picker
git clone https://9o.is/git/fzy.git
commit dfcb5e0c24ce1c2bccb9d6e3c84b730fcaf14392 parent 91b09eaa039ee255be9e18b2f97a5b96cf78c3e7 Author: Jul <jul@9o.is> Date: Wed, 6 Aug 2025 05:57:32 -0400 add partial multi-selection Diffstat:
| M | src/choices.c | | | 31 | +++++++++++++++++++++++++++++-- |
| M | src/choices.h | | | 4 | ++++ |
| M | src/options.c | | | 8 | +++++++- |
| M | src/options.h | | | 1 | + |
| M | src/tty_interface.c | | | 44 | +++++++++++++++++++++++++++++++++++++++++--- |
5 files changed, 82 insertions(+), 6 deletions(-)
diff --git a/src/choices.c b/src/choices.c @@ -94,8 +94,10 @@ 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; + free(c->multiselections); + c->selection = c->available = c->multiselection_size = 0; c->results = NULL; + c->multiselections = NULL; } void choices_init(choices_t *c, options_t *options) { @@ -127,8 +129,10 @@ void choices_destroy(choices_t *c) { c->capacity = c->size = 0; free(c->results); + free(c->multiselections); c->results = NULL; - c->available = c->selection = 0; + c->multiselections = NULL; + c->available = c->selection = c->multiselection_size = 0; } void choices_add(choices_t *c, const char *choice) { @@ -261,6 +265,14 @@ static void *choices_search_worker(void *data) { return NULL; } +void multiselection_init(choices_t *c) { + c->multiselection_size = 0; + c->multiselections = (unsigned char *) malloc(c->available * sizeof(unsigned char)); + for (size_t i = 0; i < c->available; i++) { + c->multiselections[i] = 0; + } +} + void choices_search(choices_t *c, const char *search) { choices_reset_search(c); @@ -307,6 +319,7 @@ void choices_search(choices_t *c, const char *search) { c->results = workers[0].result.list; c->available = workers[0].result.size; + multiselection_init(c); free(workers); pthread_mutex_destroy(&job->lock); free(job); @@ -333,3 +346,17 @@ void choices_next(choices_t *c) { if (c->available) c->selection = (c->selection + 1) % c->available; } + +void choices_multiselect_toggle(choices_t *c) { + if (!c->available) return; + unsigned char selected = 1 == c->multiselections[c->selection]; + + if (selected) { + c->multiselections[c->selection] = 0; + c->multiselection_size--; + } else { + c->multiselections[c->selection] = 1; + c->multiselection_size++; + } +} + diff --git a/src/choices.h b/src/choices.h @@ -28,6 +28,9 @@ typedef struct { size_t available; size_t selection; + unsigned char *multiselections; + size_t multiselection_size; + unsigned int worker_count; } choices_t; @@ -41,6 +44,7 @@ const char *choices_get(choices_t *c, size_t n); score_t choices_getscore(choices_t *c, size_t n); void choices_prev(choices_t *c); void choices_next(choices_t *c); +void choices_multiselect_toggle(choices_t *c); #ifdef __cplusplus } diff --git a/src/options.c b/src/options.c @@ -20,6 +20,7 @@ static const char *usage_str = " -0, --read-null Read input delimited by ASCII NUL characters\n" " -j, --workers NUM Use NUM workers for searching. (default is # of CPUs)\n" " -i, --show-info Show selection info line\n" + " -m, --multi Enable multi-selection\n" " -h, --help Display this help and exit\n" " -v, --version Output version information and exit\n"; @@ -38,6 +39,7 @@ static struct option longopts[] = {{"show-matches", required_argument, NULL, 'e' {"benchmark", optional_argument, NULL, 'b'}, {"workers", required_argument, NULL, 'j'}, {"show-info", no_argument, NULL, 'i'}, + {"multi", no_argument, NULL, 'm'}, {"help", no_argument, NULL, 'h'}, {NULL, 0, NULL, 0}}; @@ -53,13 +55,14 @@ void options_init(options_t *options) { options->workers = DEFAULT_WORKERS; options->input_delimiter = '\n'; options->show_info = DEFAULT_SHOW_INFO; + options->multi = 0; } void options_parse(options_t *options, int argc, char *argv[]) { options_init(options); int c; - while ((c = getopt_long(argc, argv, "vhs0e:q:l:t:p:j:i", longopts, NULL)) != -1) { + while ((c = getopt_long(argc, argv, "vhs0me:q:l:t:p:j:i", longopts, NULL)) != -1) { switch (c) { case 'v': printf("%s " VERSION " © 2014-2025 John Hawthorn\n", argv[0]); @@ -113,6 +116,9 @@ void options_parse(options_t *options, int argc, char *argv[]) { case 'i': options->show_info = 1; break; + case 'm': + options->multi = 1; + break; case 'h': default: usage(argv[0]); diff --git a/src/options.h b/src/options.h @@ -16,6 +16,7 @@ typedef struct { unsigned int workers; char input_delimiter; int show_info; + int multi; } options_t; void options_init(options_t *options); diff --git a/src/tty_interface.c b/src/tty_interface.c @@ -96,11 +96,14 @@ static void draw(tty_interface_t *state) { } } - tty_setcol(tty, 0); tty_clearline(tty); + tty_setcol(tty, options->multi ? 2 : 0); tty_printf(tty, "%s%s", options->prompt, state->search); - if (options->show_info) { + if (options->show_info && options->multi) { + tty_printf(tty, " "); + tty_printf(tty, "[%lu/%lu/%lu]", choices->multiselection_size, choices->available, choices->size); + } else if (options->show_info) { tty_printf(tty, " "); tty_printf(tty, "[%lu/%lu]", choices->available, choices->size); } @@ -110,6 +113,7 @@ static void draw(tty_interface_t *state) { tty_clearline(tty); const char *choice = choices_get(choices, i); if (choice) { + if (options->multi) tty_printf(tty, choices->multiselections[i] == 1 ? "* " : " "); draw_match(state, choice, i == choices->selection); } } @@ -117,7 +121,7 @@ static void draw(tty_interface_t *state) { if (num_lines) tty_moveup(tty, num_lines); - tty_setcol(tty, 0); + tty_setcol(tty, options->multi ? 2 : 0); fputs(options->prompt, tty->fout); for (size_t i = 0; i < state->cursor; i++) fputc(state->search[i], tty->fout); @@ -136,9 +140,41 @@ static void update_state(tty_interface_t *state) { } } +static void action_multiselect(tty_interface_t *state) { + if (!state->options->multi) return; + + choices_multiselect_toggle(state->choices); + choices_next(state->choices); + draw(state); +} + static void action_emit(tty_interface_t *state) { update_state(state); + if (state->options->multi) { + int multiselected = 0; + + for (size_t i = 0; i < state->choices->available; i++) { + if (state->choices->multiselections[i] != 1) continue; + + const char *name = choices_get(state->choices, i); + if (!name) continue; + + if (!multiselected) { + clear(state); + tty_close(state->tty); + multiselected = 1; + } + + printf("%s\n", name); + } + + if (multiselected) { + state->exit = EXIT_SUCCESS; + return; + } + } + /* Reset the tty as close as possible to the previous state */ clear(state); @@ -249,6 +285,7 @@ static void action_pagedown(tty_interface_t *state) { static void action_autocomplete(tty_interface_t *state) { update_state(state); + const char *current_selection = choices_get(state->choices, state->choices->selection); if (current_selection) { strncpy(state->search, choices_get(state->choices, state->choices->selection), SEARCH_SIZE_MAX); @@ -318,6 +355,7 @@ static const keybinding_t keybindings[] = {{"\x1b", action_exit}, /* ESC * {KEY_CTRL('J'), action_next}, /* C-J */ {KEY_CTRL('A'), action_beginning}, /* C-A */ {KEY_CTRL('E'), action_end}, /* C-E */ + {KEY_CTRL('@'), action_multiselect}, /* Space (C-@) */ {"\x1bOD", action_left}, /* LEFT */ {"\x1b[D", action_left}, /* LEFT */