Import upstream version 1.0+git20201011.1.395a253
Debian Janitor
2 years ago
0 | name: CI | |
1 | ||
2 | on: [push, pull_request] | |
3 | ||
4 | jobs: | |
5 | test: | |
6 | name: Test ${{ matrix.compiler }} on ${{ matrix.os }} | |
7 | strategy: | |
8 | matrix: | |
9 | compiler: [gcc, clang] | |
10 | os: [ubuntu-latest, macOS-latest] | |
11 | runs-on: ${{ matrix.os }} | |
12 | steps: | |
13 | - uses: actions/checkout@v2 | |
14 | - name: Compile and run tests | |
15 | run: make && make test | |
16 | env: | |
17 | CC: ${{ matrix.compiler }} | |
18 | test_alpine: | |
19 | name: Test on Alpine Linux | |
20 | runs-on: ubuntu-latest | |
21 | container: docker://alpine | |
22 | steps: | |
23 | - uses: actions/checkout@v2 | |
24 | - name: Install build dependencies | |
25 | run: apk add build-base | |
26 | - name: Compile and run tests | |
27 | run: make && make test | |
28 | multiarch_test: | |
29 | name: Test on ${{ matrix.arch }} | |
30 | strategy: | |
31 | matrix: | |
32 | arch: [armv7, aarch64, s390x, ppc64le] | |
33 | runs-on: ubuntu-20.04 | |
34 | steps: | |
35 | - uses: actions/checkout@v2 | |
36 | - uses: uraimo/run-on-arch-action@v2.0.5 | |
37 | name: Compile and run tests | |
38 | id: test | |
39 | with: | |
40 | arch: ${{ matrix.arch }} | |
41 | distro: ubuntu20.04 | |
42 | githubToken: ${{ github.token }} | |
43 | install: | | |
44 | apt-get update -q -y | |
45 | apt-get install -y gcc make | |
46 | rm -rf /var/lib/apt/lists/* | |
47 | run: make && make test | |
48 | acceptance_test: | |
49 | name: Acceptance Tests | |
50 | runs-on: ubuntu-latest | |
51 | steps: | |
52 | - uses: actions/checkout@v2 | |
53 | - name: Set up Ruby 2.6 | |
54 | uses: actions/setup-ruby@v1 | |
55 | with: | |
56 | ruby-version: 2.6.x | |
57 | - name: Install dependencies and compile | |
58 | run: | | |
59 | gem install bundler | |
60 | make | |
61 | cd test/acceptance && bundle install --jobs 4 --retry 3 | |
62 | - name: Run acceptance tests | |
63 | run: make acceptance |
0 | 0 | dist: trusty |
1 | 1 | sudo: false |
2 | 2 | language: c |
3 | compiler: | |
4 | - clang | |
5 | - gcc | |
6 | os: | |
7 | - linux | |
8 | - osx | |
3 | ||
4 | matrix: | |
5 | include: | |
6 | - os: linux | |
7 | arch: amd64 | |
8 | compiler: clang | |
9 | - os: linux | |
10 | arch: ppc64le | |
11 | compiler: clang | |
12 | - os: osx | |
13 | arch: amd64 | |
14 | compiler: clang | |
15 | - os: linux | |
16 | arch: amd64 | |
17 | compiler: gcc | |
18 | - os: linux | |
19 | arch: ppc64le | |
20 | compiler: gcc | |
21 | - os: osx | |
22 | arch: amd64 | |
23 | compiler: gcc | |
9 | 24 | script: make && make check |
10 | 25 | jobs: |
11 | 26 | include: |
0 | 0 | VERSION=1.0 |
1 | 1 | |
2 | 2 | CPPFLAGS=-DVERSION=\"${VERSION}\" -D_GNU_SOURCE |
3 | CFLAGS+=-Wall -Wextra -g -std=c99 -O3 -pedantic -Ideps | |
3 | CFLAGS+=-MD -Wall -Wextra -g -std=c99 -O3 -pedantic -Ideps -Werror=vla | |
4 | 4 | PREFIX?=/usr/local |
5 | 5 | MANDIR?=$(PREFIX)/share/man |
6 | 6 | BINDIR?=$(PREFIX)/bin |
33 | 33 | %.o: %.c config.h |
34 | 34 | $(CC) $(CPPFLAGS) $(CFLAGS) -c -o $@ $< |
35 | 35 | |
36 | config.h: | |
36 | config.h: src/config.def.h | |
37 | 37 | cp src/config.def.h config.h |
38 | 38 | |
39 | 39 | install: fzy |
48 | 48 | clang-format -i src/*.c src/*.h |
49 | 49 | |
50 | 50 | clean: |
51 | rm -f fzy test/fzytest src/*.o deps/*/*.o | |
51 | rm -f fzy test/fzytest src/*.o src/*.d deps/*/*.o | |
52 | 52 | |
53 | 53 | veryclean: clean |
54 | 54 | rm -f config.h |
55 | 55 | |
56 | 56 | .PHONY: test check all clean veryclean install fmt acceptance |
57 | ||
58 | -include $(OBJECTS:.o=.d) |
0 | 0 | ![fzy](http://i.hawth.ca/u/fzy-github.svg) |
1 | 1 | |
2 | **fzy** is a fast, simple fuzzy text selector for the terminal with an advanced [scoring | |
3 | algorithm](#sorting). | |
2 | **fzy** is a fast, simple fuzzy text selector for the terminal with an advanced scoring algorithm. | |
3 | ||
4 | [Try it out online!](http://jhawthorn.github.io/fzy-demo) | |
4 | 5 | |
5 | 6 | ![](http://i.hawth.ca/u/fzy_animated_demo.svg) |
6 | 7 | |
14 | 15 | -<a href="https://twitter.com/alexblackie/status/719297828892188672">@alexblackie</a> |
15 | 16 | </blockquote> |
16 | 17 | |
17 | [![Build Status](https://travis-ci.org/jhawthorn/fzy.svg?branch=master)](https://travis-ci.org/jhawthorn/fzy) | |
18 | [![Build Status](https://github.com/jhawthorn/fzy/workflows/CI/badge.svg)](https://github.com/jhawthorn/fzy/actions) | |
18 | 19 | |
19 | 20 | ## Why use this over fzf, pick, selecta, ctrlp, ...? |
20 | 21 | |
83 | 84 | endif |
84 | 85 | endfunction |
85 | 86 | |
86 | nnoremap <leader>e :call FzyCommand("find -type f", ":e")<cr> | |
87 | nnoremap <leader>v :call FzyCommand("find -type f", ":vs")<cr> | |
88 | nnoremap <leader>s :call FzyCommand("find -type f", ":sp")<cr> | |
87 | nnoremap <leader>e :call FzyCommand("find . -type f", ":e")<cr> | |
88 | nnoremap <leader>v :call FzyCommand("find . -type f", ":vs")<cr> | |
89 | nnoremap <leader>s :call FzyCommand("find . -type f", ":sp")<cr> | |
89 | 90 | ``` |
90 | 91 | |
91 | 92 | Any program can be used to filter files presented through fzy. [ag (the silver searcher)](https://github.com/ggreer/the_silver_searcher) can be used to ignore files specified by `.gitignore`. |
108 | 109 | |
109 | 110 | It prefers shorter candidates: `test` matches <tt><b>test</b>s</tt> over <tt><b>test</b>ing</b></tt>. |
110 | 111 | |
112 | ## See Also | |
113 | ||
114 | * [fzy.js](https://github.com/jhawthorn/fzy.js) Javascript port | |
115 | ||
116 |
22 | 22 | fatal "Unable to create a temporary directory" |
23 | 23 | fi |
24 | 24 | |
25 | mkfifo "${PATH_FIFO_IN}" "${PATH_FIFO_OUT}" | |
25 | mkfifo "${PATH_FIFO_IN}" "${PATH_FIFO_OUT}" "${PATH_FIFO_RET}" | |
26 | 26 | |
27 | 27 | export TMUX=$(_echo "${TMUX}" | cut -d , -f 1,2) |
28 | 28 | eval "tmux \ |
40 | 40 | Non-interactive mode. Print the matches in sorted order for QUERY to stdout. |
41 | 41 | . |
42 | 42 | .TP |
43 | .BR \-0 ", " \-\-read-null | |
44 | Read input delimited by ASCII NUL characters. | |
45 | . | |
46 | .TP | |
43 | 47 | .BR \-h ", " \-\-help |
44 | 48 | Usage help. |
45 | 49 | . |
53 | 57 | .BR "ENTER" |
54 | 58 | Print the selected item to stdout and exit |
55 | 59 | .TP |
56 | .BR "Ctrl+c, Esc" | |
60 | .BR "Ctrl+c, Ctrl+g, Esc" | |
57 | 61 | Exit with status 1, without making a selection. |
58 | 62 | .TP |
59 | 63 | .BR "Up Arrow, Ctrl+p" |
45 | 45 | return buffer; |
46 | 46 | } |
47 | 47 | |
48 | void choices_fread(choices_t *c, FILE *file) { | |
48 | void choices_fread(choices_t *c, FILE *file, char input_delimiter) { | |
49 | 49 | /* Save current position for parsing later */ |
50 | 50 | size_t buffer_start = c->buffer_size; |
51 | 51 | |
71 | 71 | */ |
72 | 72 | |
73 | 73 | /* Tokenize input and add to choices */ |
74 | const char *line_end = c->buffer + c->buffer_size; | |
74 | 75 | char *line = c->buffer + buffer_start; |
75 | 76 | do { |
76 | char *nl = strchr(line, '\n'); | |
77 | char *nl = strchr(line, input_delimiter); | |
77 | 78 | if (nl) |
78 | 79 | *nl++ = '\0'; |
79 | 80 | |
82 | 83 | choices_add(c, line); |
83 | 84 | |
84 | 85 | line = nl; |
85 | } while (line); | |
86 | } while (line && line < line_end); | |
86 | 87 | } |
87 | 88 | |
88 | 89 | static void choices_resize(choices_t *c, size_t new_capacity) { |
27 | 27 | } choices_t; |
28 | 28 | |
29 | 29 | void choices_init(choices_t *c, options_t *options); |
30 | void choices_fread(choices_t *c, FILE *file); | |
30 | void choices_fread(choices_t *c, FILE *file, char input_delimiter); | |
31 | 31 | void choices_destroy(choices_t *c); |
32 | 32 | void choices_add(choices_t *c, const char *choice); |
33 | 33 | size_t choices_available(choices_t *c); |
10 | 10 | |
11 | 11 | /* Time (in ms) to wait for additional bytes of an escape sequence */ |
12 | 12 | #define KEYTIMEOUT 25 |
13 | ||
14 | #define DEFAULT_TTY "/dev/tty" | |
15 | #define DEFAULT_PROMPT "> " | |
16 | #define DEFAULT_NUM_LINES 10 | |
17 | #define DEFAULT_WORKERS 0 | |
18 | #define DEFAULT_SHOW_INFO 0 |
26 | 26 | fprintf(stderr, "Must specify -e/--show-matches with --benchmark\n"); |
27 | 27 | exit(EXIT_FAILURE); |
28 | 28 | } |
29 | choices_fread(&choices, stdin); | |
29 | choices_fread(&choices, stdin, options.input_delimiter); | |
30 | 30 | for (int i = 0; i < options.benchmark; i++) |
31 | 31 | choices_search(&choices, options.filter); |
32 | 32 | } else if (options.filter) { |
33 | choices_fread(&choices, stdin); | |
33 | choices_fread(&choices, stdin, options.input_delimiter); | |
34 | 34 | choices_search(&choices, options.filter); |
35 | 35 | for (size_t i = 0; i < choices_available(&choices); i++) { |
36 | 36 | if (options.show_scores) |
41 | 41 | /* interactive */ |
42 | 42 | |
43 | 43 | if (isatty(STDIN_FILENO)) |
44 | choices_fread(&choices, stdin); | |
44 | choices_fread(&choices, stdin, options.input_delimiter); | |
45 | 45 | |
46 | 46 | tty_t tty; |
47 | 47 | tty_init(&tty, options.tty_filename); |
48 | 48 | |
49 | 49 | if (!isatty(STDIN_FILENO)) |
50 | choices_fread(&choices, stdin); | |
50 | choices_fread(&choices, stdin, options.input_delimiter); | |
51 | 51 | |
52 | 52 | if (options.num_lines > choices.size) |
53 | 53 | options.num_lines = choices.size; |
54 | 54 | |
55 | if (options.num_lines + 1 > tty_getheight(&tty)) | |
56 | options.num_lines = tty_getheight(&tty) - 1; | |
55 | int num_lines_adjustment = 1; | |
56 | if (options.show_info) | |
57 | num_lines_adjustment++; | |
58 | ||
59 | if (options.num_lines + num_lines_adjustment > tty_getheight(&tty)) | |
60 | options.num_lines = tty_getheight(&tty) - num_lines_adjustment; | |
57 | 61 | |
58 | 62 | tty_interface_t tty_interface; |
59 | 63 | tty_interface_init(&tty_interface, &tty, &choices, &options); |
3 | 3 | #include <stdio.h> |
4 | 4 | #include <float.h> |
5 | 5 | #include <math.h> |
6 | #include <stdlib.h> | |
6 | 7 | |
7 | 8 | #include "match.h" |
8 | 9 | #include "bonus.h" |
26 | 27 | return 1; |
27 | 28 | } |
28 | 29 | |
30 | #define SWAP(x, y, T) do { T SWAP = x; x = y; y = SWAP; } while (0) | |
31 | ||
29 | 32 | #define max(a, b) (((a) > (b)) ? (a) : (b)) |
30 | 33 | |
31 | #ifdef DEBUG_VERBOSE | |
32 | /* print one of the internal matrices */ | |
33 | void mat_print(score_t *mat, char name, const char *needle, const char *haystack) { | |
34 | int n = strlen(needle); | |
35 | int m = strlen(haystack); | |
36 | int i, j; | |
37 | fprintf(stderr, "%c ", name); | |
38 | for (j = 0; j < m; j++) { | |
39 | fprintf(stderr, " %c", haystack[j]); | |
40 | } | |
41 | fprintf(stderr, "\n"); | |
42 | for (i = 0; i < n; i++) { | |
43 | fprintf(stderr, " %c |", needle[i]); | |
44 | for (j = 0; j < m; j++) { | |
45 | score_t val = mat[i * m + j]; | |
46 | if (val == SCORE_MIN) { | |
47 | fprintf(stderr, " -\u221E"); | |
48 | } else { | |
49 | fprintf(stderr, " %.3f", val); | |
50 | } | |
51 | } | |
52 | fprintf(stderr, "\n"); | |
53 | } | |
54 | fprintf(stderr, "\n\n"); | |
55 | } | |
56 | #endif | |
34 | struct match_struct { | |
35 | int needle_len; | |
36 | int haystack_len; | |
37 | ||
38 | char lower_needle[MATCH_MAX_LEN]; | |
39 | char lower_haystack[MATCH_MAX_LEN]; | |
40 | ||
41 | score_t match_bonus[MATCH_MAX_LEN]; | |
42 | }; | |
57 | 43 | |
58 | 44 | static void precompute_bonus(const char *haystack, score_t *match_bonus) { |
59 | 45 | /* Which positions are beginning of words */ |
60 | int m = strlen(haystack); | |
61 | 46 | char last_ch = '/'; |
62 | for (int i = 0; i < m; i++) { | |
47 | for (int i = 0; haystack[i]; i++) { | |
63 | 48 | char ch = haystack[i]; |
64 | 49 | match_bonus[i] = COMPUTE_BONUS(last_ch, ch); |
65 | 50 | last_ch = ch; |
66 | 51 | } |
67 | 52 | } |
68 | 53 | |
54 | static void setup_match_struct(struct match_struct *match, const char *needle, const char *haystack) { | |
55 | match->needle_len = strlen(needle); | |
56 | match->haystack_len = strlen(haystack); | |
57 | ||
58 | if (match->haystack_len > MATCH_MAX_LEN || match->needle_len > match->haystack_len) { | |
59 | return; | |
60 | } | |
61 | ||
62 | for (int i = 0; i < match->needle_len; i++) | |
63 | match->lower_needle[i] = tolower(needle[i]); | |
64 | ||
65 | for (int i = 0; i < match->haystack_len; i++) | |
66 | match->lower_haystack[i] = tolower(haystack[i]); | |
67 | ||
68 | precompute_bonus(haystack, match->match_bonus); | |
69 | } | |
70 | ||
71 | static inline void match_row(const struct match_struct *match, int row, score_t *curr_D, score_t *curr_M, const score_t *last_D, const score_t *last_M) { | |
72 | int n = match->needle_len; | |
73 | int m = match->haystack_len; | |
74 | int i = row; | |
75 | ||
76 | const char *lower_needle = match->lower_needle; | |
77 | const char *lower_haystack = match->lower_haystack; | |
78 | const score_t *match_bonus = match->match_bonus; | |
79 | ||
80 | score_t prev_score = SCORE_MIN; | |
81 | score_t gap_score = i == n - 1 ? SCORE_GAP_TRAILING : SCORE_GAP_INNER; | |
82 | ||
83 | for (int j = 0; j < m; j++) { | |
84 | if (lower_needle[i] == lower_haystack[j]) { | |
85 | score_t score = SCORE_MIN; | |
86 | if (!i) { | |
87 | score = (j * SCORE_GAP_LEADING) + match_bonus[j]; | |
88 | } else if (j) { /* i > 0 && j > 0*/ | |
89 | score = max( | |
90 | last_M[j - 1] + match_bonus[j], | |
91 | ||
92 | /* consecutive match, doesn't stack with match_bonus */ | |
93 | last_D[j - 1] + SCORE_MATCH_CONSECUTIVE); | |
94 | } | |
95 | curr_D[j] = score; | |
96 | curr_M[j] = prev_score = max(score, prev_score + gap_score); | |
97 | } else { | |
98 | curr_D[j] = SCORE_MIN; | |
99 | curr_M[j] = prev_score = prev_score + gap_score; | |
100 | } | |
101 | } | |
102 | } | |
103 | ||
104 | score_t match(const char *needle, const char *haystack) { | |
105 | if (!*needle) | |
106 | return SCORE_MIN; | |
107 | ||
108 | struct match_struct match; | |
109 | setup_match_struct(&match, needle, haystack); | |
110 | ||
111 | int n = match.needle_len; | |
112 | int m = match.haystack_len; | |
113 | ||
114 | if (m > MATCH_MAX_LEN || n > m) { | |
115 | /* | |
116 | * Unreasonably large candidate: return no score | |
117 | * If it is a valid match it will still be returned, it will | |
118 | * just be ranked below any reasonably sized candidates | |
119 | */ | |
120 | return SCORE_MIN; | |
121 | } else if (n == m) { | |
122 | /* Since this method can only be called with a haystack which | |
123 | * matches needle. If the lengths of the strings are equal the | |
124 | * strings themselves must also be equal (ignoring case). | |
125 | */ | |
126 | return SCORE_MAX; | |
127 | } | |
128 | ||
129 | /* | |
130 | * D[][] Stores the best score for this position ending with a match. | |
131 | * M[][] Stores the best possible score at this position. | |
132 | */ | |
133 | score_t D[2][MATCH_MAX_LEN], M[2][MATCH_MAX_LEN]; | |
134 | ||
135 | score_t *last_D, *last_M; | |
136 | score_t *curr_D, *curr_M; | |
137 | ||
138 | last_D = D[0]; | |
139 | last_M = M[0]; | |
140 | curr_D = D[1]; | |
141 | curr_M = M[1]; | |
142 | ||
143 | for (int i = 0; i < n; i++) { | |
144 | match_row(&match, i, curr_D, curr_M, last_D, last_M); | |
145 | ||
146 | SWAP(curr_D, last_D, score_t *); | |
147 | SWAP(curr_M, last_M, score_t *); | |
148 | } | |
149 | ||
150 | return last_M[m - 1]; | |
151 | } | |
152 | ||
69 | 153 | score_t match_positions(const char *needle, const char *haystack, size_t *positions) { |
70 | 154 | if (!*needle) |
71 | 155 | return SCORE_MIN; |
72 | 156 | |
73 | int n = strlen(needle); | |
74 | int m = strlen(haystack); | |
75 | ||
76 | if (n == m) { | |
157 | struct match_struct match; | |
158 | setup_match_struct(&match, needle, haystack); | |
159 | ||
160 | int n = match.needle_len; | |
161 | int m = match.haystack_len; | |
162 | ||
163 | if (m > MATCH_MAX_LEN || n > m) { | |
164 | /* | |
165 | * Unreasonably large candidate: return no score | |
166 | * If it is a valid match it will still be returned, it will | |
167 | * just be ranked below any reasonably sized candidates | |
168 | */ | |
169 | return SCORE_MIN; | |
170 | } else if (n == m) { | |
77 | 171 | /* Since this method can only be called with a haystack which |
78 | 172 | * matches needle. If the lengths of the strings are equal the |
79 | 173 | * strings themselves must also be equal (ignoring case). |
84 | 178 | return SCORE_MAX; |
85 | 179 | } |
86 | 180 | |
87 | if (m > 1024) { | |
88 | /* | |
89 | * Unreasonably large candidate: return no score | |
90 | * If it is a valid match it will still be returned, it will | |
91 | * just be ranked below any reasonably sized candidates | |
92 | */ | |
93 | return SCORE_MIN; | |
94 | } | |
95 | ||
96 | score_t match_bonus[m]; | |
97 | score_t D[n][m], M[n][m]; | |
98 | ||
99 | 181 | /* |
100 | 182 | * D[][] Stores the best score for this position ending with a match. |
101 | 183 | * M[][] Stores the best possible score at this position. |
102 | 184 | */ |
103 | precompute_bonus(haystack, match_bonus); | |
185 | score_t (*D)[MATCH_MAX_LEN], (*M)[MATCH_MAX_LEN]; | |
186 | M = malloc(sizeof(score_t) * MATCH_MAX_LEN * n); | |
187 | D = malloc(sizeof(score_t) * MATCH_MAX_LEN * n); | |
188 | ||
189 | score_t *last_D, *last_M; | |
190 | score_t *curr_D, *curr_M; | |
104 | 191 | |
105 | 192 | for (int i = 0; i < n; i++) { |
106 | score_t prev_score = SCORE_MIN; | |
107 | score_t gap_score = i == n - 1 ? SCORE_GAP_TRAILING : SCORE_GAP_INNER; | |
108 | ||
109 | for (int j = 0; j < m; j++) { | |
110 | if (tolower(needle[i]) == tolower(haystack[j])) { | |
111 | score_t score = SCORE_MIN; | |
112 | if (!i) { | |
113 | score = (j * SCORE_GAP_LEADING) + match_bonus[j]; | |
114 | } else if (j) { /* i > 0 && j > 0*/ | |
115 | score = max( | |
116 | M[i - 1][j - 1] + match_bonus[j], | |
117 | ||
118 | /* consecutive match, doesn't stack with match_bonus */ | |
119 | D[i - 1][j - 1] + SCORE_MATCH_CONSECUTIVE); | |
120 | } | |
121 | D[i][j] = score; | |
122 | M[i][j] = prev_score = max(score, prev_score + gap_score); | |
123 | } else { | |
124 | D[i][j] = SCORE_MIN; | |
125 | M[i][j] = prev_score = prev_score + gap_score; | |
126 | } | |
127 | } | |
128 | } | |
129 | ||
130 | #ifdef DEBUG_VERBOSE | |
131 | fprintf(stderr, "\"%s\" =~ \"%s\"\n", needle, haystack); | |
132 | mat_print(&D[0][0], 'D', needle, haystack); | |
133 | mat_print(&M[0][0], 'M', needle, haystack); | |
134 | fprintf(stderr, "\n"); | |
135 | #endif | |
193 | curr_D = &D[i][0]; | |
194 | curr_M = &M[i][0]; | |
195 | ||
196 | match_row(&match, i, curr_D, curr_M, last_D, last_M); | |
197 | ||
198 | last_D = curr_D; | |
199 | last_M = curr_M; | |
200 | } | |
136 | 201 | |
137 | 202 | /* backtrace to find the positions of optimal matching */ |
138 | 203 | if (positions) { |
163 | 228 | } |
164 | 229 | } |
165 | 230 | |
166 | return M[n - 1][m - 1]; | |
167 | } | |
168 | ||
169 | score_t match(const char *needle, const char *haystack) { | |
170 | return match_positions(needle, haystack, NULL); | |
171 | } | |
231 | score_t result = M[n - 1][m - 1]; | |
232 | ||
233 | free(M); | |
234 | free(D); | |
235 | ||
236 | return result; | |
237 | } |
6 | 6 | #define SCORE_MAX INFINITY |
7 | 7 | #define SCORE_MIN -INFINITY |
8 | 8 | |
9 | #define MATCH_MAX_LEN 1024 | |
10 | ||
9 | 11 | int has_match(const char *needle, const char *haystack); |
10 | 12 | score_t match_positions(const char *needle, const char *haystack, size_t *positions); |
11 | 13 | score_t match(const char *needle, const char *haystack); |
4 | 4 | #include <string.h> |
5 | 5 | |
6 | 6 | #include "options.h" |
7 | ||
8 | #include "../config.h" | |
7 | 9 | |
8 | 10 | static const char *usage_str = |
9 | 11 | "" |
14 | 16 | " -e, --show-matches=QUERY Output the sorted matches of QUERY\n" |
15 | 17 | " -t, --tty=TTY Specify file to use as TTY device (default /dev/tty)\n" |
16 | 18 | " -s, --show-scores Show the scores of each match\n" |
19 | " -0, --read-null Read input delimited by ASCII NUL characters\n" | |
17 | 20 | " -j, --workers NUM Use NUM workers for searching. (default is # of CPUs)\n" |
21 | " -i, --show-info Show selection info line\n" | |
18 | 22 | " -h, --help Display this help and exit\n" |
19 | 23 | " -v, --version Output version information and exit\n"; |
20 | 24 | |
28 | 32 | {"tty", required_argument, NULL, 't'}, |
29 | 33 | {"prompt", required_argument, NULL, 'p'}, |
30 | 34 | {"show-scores", no_argument, NULL, 's'}, |
35 | {"read-null", no_argument, NULL, '0'}, | |
31 | 36 | {"version", no_argument, NULL, 'v'}, |
32 | 37 | {"benchmark", optional_argument, NULL, 'b'}, |
33 | 38 | {"workers", required_argument, NULL, 'j'}, |
39 | {"show-info", no_argument, NULL, 'i'}, | |
34 | 40 | {"help", no_argument, NULL, 'h'}, |
35 | 41 | {NULL, 0, NULL, 0}}; |
36 | 42 | |
37 | 43 | void options_init(options_t *options) { |
38 | 44 | /* set defaults */ |
39 | options->benchmark = 0; | |
40 | options->filter = NULL; | |
41 | options->init_search = NULL; | |
42 | options->tty_filename = "/dev/tty"; | |
43 | options->show_scores = 0; | |
44 | options->num_lines = 10; | |
45 | options->scrolloff = 1; | |
46 | options->prompt = "> "; | |
47 | options->workers = 0; | |
45 | options->benchmark = 0; | |
46 | options->filter = NULL; | |
47 | options->init_search = NULL; | |
48 | options->show_scores = 0; | |
49 | options->scrolloff = 1; | |
50 | options->tty_filename = DEFAULT_TTY; | |
51 | options->num_lines = DEFAULT_NUM_LINES; | |
52 | options->prompt = DEFAULT_PROMPT; | |
53 | options->workers = DEFAULT_WORKERS; | |
54 | options->input_delimiter = '\n'; | |
55 | options->show_info = DEFAULT_SHOW_INFO; | |
48 | 56 | } |
49 | 57 | |
50 | 58 | void options_parse(options_t *options, int argc, char *argv[]) { |
51 | 59 | options_init(options); |
52 | 60 | |
53 | 61 | int c; |
54 | while ((c = getopt_long(argc, argv, "vhse:q:l:t:p:j:", longopts, NULL)) != -1) { | |
62 | while ((c = getopt_long(argc, argv, "vhs0e:q:l:t:p:j:i", longopts, NULL)) != -1) { | |
55 | 63 | switch (c) { |
56 | 64 | case 'v': |
57 | 65 | printf("%s " VERSION " © 2014-2018 John Hawthorn\n", argv[0]); |
58 | 66 | exit(EXIT_SUCCESS); |
59 | 67 | case 's': |
60 | 68 | options->show_scores = 1; |
69 | break; | |
70 | case '0': | |
71 | options->input_delimiter = '\0'; | |
61 | 72 | break; |
62 | 73 | case 'q': |
63 | 74 | options->init_search = optarg; |
99 | 110 | } |
100 | 111 | options->num_lines = l; |
101 | 112 | } break; |
113 | case 'i': | |
114 | options->show_info = 1; | |
115 | break; | |
102 | 116 | case 'h': |
103 | 117 | default: |
104 | 118 | usage(argv[0]); |
10 | 10 | unsigned int scrolloff; |
11 | 11 | const char *prompt; |
12 | 12 | unsigned int workers; |
13 | char input_delimiter; | |
14 | int show_info; | |
13 | 15 | } options_t; |
14 | 16 | |
15 | 17 | void options_init(options_t *options); |
183 | 183 | va_end(args); |
184 | 184 | } |
185 | 185 | |
186 | void tty_putc(tty_t *tty, char c) { | |
187 | fputc(c, tty->fout); | |
188 | } | |
189 | ||
186 | 190 | void tty_flush(tty_t *tty) { |
187 | 191 | fflush(tty->fout); |
188 | 192 | } |
50 | 50 | void tty_setcol(tty_t *tty, int col); |
51 | 51 | |
52 | 52 | void tty_printf(tty_t *tty, const char *fmt, ...); |
53 | void tty_putc(tty_t *tty, char c); | |
53 | 54 | void tty_flush(tty_t *tty); |
54 | 55 | |
55 | 56 | size_t tty_getwidth(tty_t *tty); |
19 | 19 | |
20 | 20 | tty_setcol(tty, 0); |
21 | 21 | size_t line = 0; |
22 | while (line++ < state->options->num_lines) { | |
22 | while (line++ < state->options->num_lines + (state->options->show_info ? 1 : 0)) { | |
23 | 23 | tty_newline(tty); |
24 | 24 | } |
25 | 25 | tty_clearline(tty); |
35 | 35 | char *search = state->last_search; |
36 | 36 | |
37 | 37 | int n = strlen(search); |
38 | size_t positions[n + 1]; | |
39 | for (int i = 0; i < n + 1; i++) | |
38 | size_t positions[MATCH_MAX_LEN]; | |
39 | for (int i = 0; i < n + 1 && i < MATCH_MAX_LEN; i++) | |
40 | 40 | positions[i] = -1; |
41 | 41 | |
42 | 42 | score_t score = match_positions(search, choice, &positions[0]); |
64 | 64 | } else { |
65 | 65 | tty_setfg(tty, TTY_COLOR_NORMAL); |
66 | 66 | } |
67 | tty_printf(tty, "%c", choice[i]); | |
67 | if (choice[i] == '\n') { | |
68 | tty_putc(tty, ' '); | |
69 | } else { | |
70 | tty_printf(tty, "%c", choice[i]); | |
71 | } | |
68 | 72 | } |
69 | 73 | tty_setwrap(tty); |
70 | 74 | tty_setnormal(tty); |
85 | 89 | start = available - num_lines; |
86 | 90 | } |
87 | 91 | } |
92 | ||
88 | 93 | tty_setcol(tty, 0); |
89 | 94 | tty_printf(tty, "%s%s", options->prompt, state->search); |
90 | 95 | tty_clearline(tty); |
96 | ||
97 | if (options->show_info) { | |
98 | tty_printf(tty, "\n[%lu/%lu]", choices->available, choices->size); | |
99 | tty_clearline(tty); | |
100 | } | |
101 | ||
91 | 102 | for (size_t i = start; i < start + num_lines; i++) { |
92 | 103 | tty_printf(tty, "\n"); |
93 | 104 | tty_clearline(tty); |
96 | 107 | draw_match(state, choice, i == choices->selection); |
97 | 108 | } |
98 | 109 | } |
99 | if (num_lines > 0) { | |
100 | tty_moveup(tty, num_lines); | |
101 | } | |
110 | ||
111 | if (num_lines + options->show_info) | |
112 | tty_moveup(tty, num_lines + options->show_info); | |
102 | 113 | |
103 | 114 | tty_setcol(tty, 0); |
104 | 115 | fputs(options->prompt, tty->fout); |
286 | 297 | {KEY_CTRL('I'), action_autocomplete}, /* TAB (C-I ) */ |
287 | 298 | {KEY_CTRL('C'), action_exit}, /* C-C */ |
288 | 299 | {KEY_CTRL('D'), action_exit}, /* C-D */ |
300 | {KEY_CTRL('G'), action_exit}, /* C-G */ | |
289 | 301 | {KEY_CTRL('M'), action_emit}, /* CR */ |
290 | 302 | {KEY_CTRL('P'), action_prev}, /* C-P */ |
291 | 303 | {KEY_CTRL('N'), action_next}, /* C-N */ |
0 | # coding: utf-8 | |
0 | 1 | require 'minitest' |
1 | 2 | require 'minitest/autorun' |
2 | 3 | require 'ttytest' |
443 | 444 | LongStringOfTextLongStringOfTextLongStringOfTextLongStringOfTextLongStringOfText |
444 | 445 | LongStringOfTextLongStringOfTextLongStri |
445 | 446 | TTY |
447 | end | |
448 | ||
449 | def test_show_info | |
450 | @tty = interactive_fzy(input: %w[foo bar baz], args: "-i") | |
451 | @tty.assert_matches ">\n[3/3]\nfoo\nbar\nbaz" | |
452 | @tty.send_keys("ba") | |
453 | @tty.assert_matches "> ba\n[2/3]\nbar\nbaz" | |
454 | @tty.send_keys("q") | |
455 | @tty.assert_matches "> baq\n[0/3]" | |
446 | 456 | end |
447 | 457 | |
448 | 458 | def test_help |
455 | 465 | -e, --show-matches=QUERY Output the sorted matches of QUERY |
456 | 466 | -t, --tty=TTY Specify file to use as TTY device (default /dev/tty) |
457 | 467 | -s, --show-scores Show the scores of each match |
468 | -0, --read-null Read input delimited by ASCII NUL characters | |
458 | 469 | -j, --workers NUM Use NUM workers for searching. (default is # of CPUs) |
470 | -i, --show-info Show selection info line | |
459 | 471 | -h, --help Display this help and exit |
460 | 472 | -v, --version Output version information and exit |
461 | 473 | TTY |
134 | 134 | } |
135 | 135 | |
136 | 136 | TEST test_choices_large_input() { |
137 | int N = 100000; | |
138 | char *strings[N]; | |
137 | const int N = 100000; | |
138 | char *strings[100000]; | |
139 | 139 | |
140 | 140 | for(int i = 0; i < N; i++) { |
141 | 141 | asprintf(&strings[i], "%i", i); |
127 | 127 | ASSERT_SCORE_EQ(SCORE_GAP_LEADING + SCORE_MATCH_DOT, match("a", ".a")); |
128 | 128 | ASSERT_SCORE_EQ(SCORE_GAP_LEADING*3 + SCORE_MATCH_DOT, match("a", "*a.a")); |
129 | 129 | ASSERT_SCORE_EQ(SCORE_GAP_LEADING + SCORE_GAP_INNER + SCORE_MATCH_DOT, match("a", "*a.a")); |
130 | PASS(); | |
131 | } | |
132 | ||
133 | TEST score_long_string() { | |
134 | char string[4096]; | |
135 | memset(string, 'a', sizeof(string) - 1); | |
136 | string[sizeof(string) - 1] = '\0'; | |
137 | ||
138 | ASSERT_SCORE_EQ(SCORE_MIN, match("aa", string)); | |
139 | ASSERT_SCORE_EQ(SCORE_MIN, match(string, "aa")); | |
140 | ASSERT_SCORE_EQ(SCORE_MIN, match(string, string)); | |
141 | ||
130 | 142 | PASS(); |
131 | 143 | } |
132 | 144 | |
209 | 221 | RUN_TEST(score_slash); |
210 | 222 | RUN_TEST(score_capital); |
211 | 223 | RUN_TEST(score_dot); |
224 | RUN_TEST(score_long_string); | |
212 | 225 | |
213 | 226 | RUN_TEST(positions_consecutive); |
214 | 227 | RUN_TEST(positions_start_of_word); |