fzy
terminal fuzzy finder picker
git clone https://9o.is/git/fzy.git
choices.c
(9079B)
1 #include <stdlib.h>
2 #include <stdio.h>
3 #include <string.h>
4 #include <pthread.h>
5 #include <unistd.h>
6 #include <errno.h>
7
8 #include "options.h"
9 #include "choices.h"
10 #include "match.h"
11
12 /* Initial size of buffer for storing input in memory */
13 #define INITIAL_BUFFER_CAPACITY 4096
14
15 /* Initial size of choices array */
16 #define INITIAL_CHOICE_CAPACITY 128
17
18 static int cmpchoice(const void *_idx1, const void *_idx2) {
19 const struct scored_result *a = _idx1;
20 const struct scored_result *b = _idx2;
21
22 if (a->score == b->score) {
23 /* To ensure a stable sort, we must also sort by the string
24 * pointers. We can do this since we know all the strings are
25 * from a contiguous memory segment (buffer in choices_t).
26 */
27 if (a->str < b->str) {
28 return -1;
29 } else {
30 return 1;
31 }
32 } else if (a->score < b->score) {
33 return 1;
34 } else {
35 return -1;
36 }
37 }
38
39 static void *safe_realloc(void *buffer, size_t size) {
40 buffer = realloc(buffer, size);
41 if (!buffer) {
42 fprintf(stderr, "Error: Can't allocate memory (%zu bytes)\n", size);
43 abort();
44 }
45
46 return buffer;
47 }
48
49 static void multiselection_init(choices_t *c) {
50 c->multiselection_size = 0;
51 c->multiselections = (unsigned char *) malloc(c->size * sizeof(unsigned char));
52 for (size_t i = 0; i < c->size; i++) {
53 c->multiselections[i] = 0;
54 }
55 }
56
57 void choices_fread(choices_t *c, FILE *file, char input_delimiter) {
58 /* Save current position for parsing later */
59 size_t buffer_start = c->buffer_size;
60
61 /* Resize buffer to at least one byte more capacity than our current
62 * size. This uses a power of two of INITIAL_BUFFER_CAPACITY.
63 * This must work even when c->buffer is NULL and c->buffer_size is 0
64 */
65 size_t capacity = INITIAL_BUFFER_CAPACITY;
66 while (capacity <= c->buffer_size)
67 capacity *= 2;
68 c->buffer = safe_realloc(c->buffer, capacity);
69
70 /* Continue reading until we get a "short" read, indicating EOF */
71 while ((c->buffer_size += fread(c->buffer + c->buffer_size, 1, capacity - c->buffer_size, file)) == capacity) {
72 capacity *= 2;
73 c->buffer = safe_realloc(c->buffer, capacity);
74 }
75 c->buffer = safe_realloc(c->buffer, c->buffer_size + 1);
76 c->buffer[c->buffer_size++] = '\0';
77
78 /* Truncate buffer to used size, (maybe) freeing some memory for
79 * future allocations.
80 */
81
82 /* Tokenize input and add to choices */
83 const char *line_end = c->buffer + c->buffer_size;
84 char *line = c->buffer + buffer_start;
85 do {
86 char *nl = strchr(line, input_delimiter);
87 if (nl)
88 *nl++ = '\0';
89
90 /* Skip empty lines */
91 if (*line)
92 choices_add(c, line);
93
94 line = nl;
95 } while (line && line < line_end);
96
97 multiselection_init(c);
98 }
99
100 static void choices_resize(choices_t *c, size_t new_capacity) {
101 c->strings = safe_realloc(c->strings, new_capacity * sizeof(const char *));
102 c->capacity = new_capacity;
103 }
104
105 static void choices_reset_search(choices_t *c) {
106 free(c->results);
107 c->selection = c->available = 0;
108 c->results = NULL;
109 }
110
111 void choices_init(choices_t *c, options_t *options) {
112 c->strings = NULL;
113 c->results = NULL;
114
115 c->buffer_size = 0;
116 c->buffer = NULL;
117
118 c->capacity = c->size = 0;
119 choices_resize(c, INITIAL_CHOICE_CAPACITY);
120
121 if (options->workers) {
122 c->worker_count = options->workers;
123 } else {
124 c->worker_count = (int)sysconf(_SC_NPROCESSORS_ONLN);
125 }
126
127 choices_reset_search(c);
128 }
129
130 void choices_destroy(choices_t *c) {
131 free(c->buffer);
132 c->buffer = NULL;
133 c->buffer_size = 0;
134
135 free(c->strings);
136 c->strings = NULL;
137 c->capacity = c->size = 0;
138
139 free(c->results);
140 free(c->multiselections);
141 c->results = NULL;
142 c->multiselections = NULL;
143 c->available = c->selection = c->multiselection_size = 0;
144 }
145
146 void choices_add(choices_t *c, const char *choice) {
147 /* Previous search is now invalid */
148 choices_reset_search(c);
149
150 if (c->size == c->capacity) {
151 choices_resize(c, c->capacity * 2);
152 }
153 c->strings[c->size++] = choice;
154 }
155
156 size_t choices_available(choices_t *c) {
157 return c->available;
158 }
159
160 #define BATCH_SIZE 512
161
162 struct result_list {
163 struct scored_result *list;
164 size_t size;
165 };
166
167 struct search_job {
168 pthread_mutex_t lock;
169 choices_t *choices;
170 const char *search;
171 size_t processed;
172 struct worker *workers;
173 };
174
175 struct worker {
176 pthread_t thread_id;
177 struct search_job *job;
178 unsigned int worker_num;
179 struct result_list result;
180 };
181
182 static void worker_get_next_batch(struct search_job *job, size_t *start, size_t *end) {
183 pthread_mutex_lock(&job->lock);
184
185 *start = job->processed;
186
187 job->processed += BATCH_SIZE;
188 if (job->processed > job->choices->size) {
189 job->processed = job->choices->size;
190 }
191
192 *end = job->processed;
193
194 pthread_mutex_unlock(&job->lock);
195 }
196
197 static struct result_list merge2(struct result_list list1, struct result_list list2) {
198 size_t result_index = 0, index1 = 0, index2 = 0;
199
200 struct result_list result;
201 result.size = list1.size + list2.size;
202 result.list = malloc(result.size * sizeof(struct scored_result));
203 if (!result.list) {
204 fprintf(stderr, "Error: Can't allocate memory\n");
205 abort();
206 }
207
208 while(index1 < list1.size && index2 < list2.size) {
209 if (cmpchoice(&list1.list[index1], &list2.list[index2]) < 0) {
210 result.list[result_index++] = list1.list[index1++];
211 } else {
212 result.list[result_index++] = list2.list[index2++];
213 }
214 }
215
216 while(index1 < list1.size) {
217 result.list[result_index++] = list1.list[index1++];
218 }
219 while(index2 < list2.size) {
220 result.list[result_index++] = list2.list[index2++];
221 }
222
223 free(list1.list);
224 free(list2.list);
225
226 return result;
227 }
228
229 static void *choices_search_worker(void *data) {
230 struct worker *w = (struct worker *)data;
231 struct search_job *job = w->job;
232 const choices_t *c = job->choices;
233 struct result_list *result = &w->result;
234
235 size_t start, end;
236
237 for(;;) {
238 worker_get_next_batch(job, &start, &end);
239
240 if(start == end) {
241 break;
242 }
243
244 for(size_t i = start; i < end; i++) {
245 if (has_match(job->search, c->strings[i])) {
246 result->list[result->size].index = i;
247 result->list[result->size].str = c->strings[i];
248 result->list[result->size].score = match(job->search, c->strings[i]);
249 result->size++;
250 }
251 }
252 }
253
254 /* Sort the partial result */
255 qsort(result->list, result->size, sizeof(struct scored_result), cmpchoice);
256
257 /* Fan-in, merging results */
258 for(unsigned int step = 0;; step++) {
259 if (w->worker_num % (2 << step))
260 break;
261
262 unsigned int next_worker = w->worker_num | (1 << step);
263 if (next_worker >= c->worker_count)
264 break;
265
266 if ((errno = pthread_join(job->workers[next_worker].thread_id, NULL))) {
267 perror("pthread_join");
268 exit(EXIT_FAILURE);
269 }
270
271 w->result = merge2(w->result, job->workers[next_worker].result);
272 }
273
274 return NULL;
275 }
276
277 void choices_search(choices_t *c, const char *search) {
278 choices_reset_search(c);
279
280 struct search_job *job = calloc(1, sizeof(struct search_job));
281 if (!job) {
282 fprintf(stderr, "Error: Can't allocate memory\n");
283 abort();
284 }
285 job->search = search;
286 job->choices = c;
287 if (pthread_mutex_init(&job->lock, NULL) != 0) {
288 fprintf(stderr, "Error: pthread_mutex_init failed\n");
289 abort();
290 }
291 job->workers = calloc(c->worker_count, sizeof(struct worker));
292 if (!job->workers) {
293 fprintf(stderr, "Error: Can't allocate memory\n");
294 abort();
295 }
296
297 struct worker *workers = job->workers;
298 for (int i = c->worker_count - 1; i >= 0; i--) {
299 workers[i].job = job;
300 workers[i].worker_num = i;
301 workers[i].result.size = 0;
302 workers[i].result.list = malloc(c->size * sizeof(struct scored_result)); /* FIXME: This is overkill */
303 if (!workers[i].result.list) {
304 fprintf(stderr, "Error: Can't allocate memory\n");
305 abort();
306 }
307
308 /* These must be created last-to-first to avoid a race condition when fanning in */
309 if ((errno = pthread_create(&workers[i].thread_id, NULL, &choices_search_worker, &workers[i]))) {
310 perror("pthread_create");
311 exit(EXIT_FAILURE);
312 }
313 }
314
315 if (pthread_join(workers[0].thread_id, NULL)) {
316 perror("pthread_join");
317 exit(EXIT_FAILURE);
318 }
319
320 c->results = workers[0].result.list;
321 c->available = workers[0].result.size;
322
323 free(workers);
324 pthread_mutex_destroy(&job->lock);
325 free(job);
326 }
327
328 const char *choices_get(choices_t *c, size_t n) {
329 if (n < c->available) {
330 return c->results[n].str;
331 } else {
332 return NULL;
333 }
334 }
335
336 const char *choices_getmulti(choices_t *c, size_t n) {
337 if (n < c->size) {
338 return c->strings[n];
339 } else {
340 return NULL;
341 }
342 }
343
344 score_t choices_getscore(choices_t *c, size_t n) {
345 return c->results[n].score;
346 }
347
348 void choices_prev(choices_t *c) {
349 if (c->available)
350 c->selection = (c->selection + c->available - 1) % c->available;
351 }
352
353 void choices_next(choices_t *c) {
354 if (c->available)
355 c->selection = (c->selection + 1) % c->available;
356 }
357
358 void choices_multiselect_toggle(choices_t *c) {
359 if (!c->available) return;
360 size_t index = c->results[c->selection].index;
361
362 if (c->multiselections[index]) {
363 c->multiselections[index] = 0;
364 c->multiselection_size--;
365 } else {
366 c->multiselections[index] = 1;
367 c->multiselection_size++;
368 }
369 }
370
371 size_t choices_selected(choices_t *c, size_t n) {
372 if (!c->available) return 0;
373 size_t index = c->results[n].index;
374 return c->multiselections[index];
375 }