add readline history support
[ssp.git] / src / kjv.c
blob9f708bcfb377a45874a3902c6f7ff47be8b2060d
1 /*
2 kjv: Read the Word of God from your terminal
4 License: Public domain
5 */
7 #include <assert.h>
8 #include <stdio.h>
9 #include <stdlib.h>
10 #include <unistd.h>
11 #include <stdbool.h>
12 #include <string.h>
13 #include <signal.h>
14 #include <readline/readline.h>
15 #include <readline/history.h>
16 #include <regex.h>
17 #include <sys/types.h>
18 #include <sys/wait.h>
19 #include <ctype.h>
20 #include <sys/ioctl.h>
22 #include "data.h"
23 #include "intset.h"
25 typedef struct {
26 bool linewrap;
27 int maximum_line_length;
29 int context_before;
30 int context_after;
31 bool context_chapter;
32 } kjv_config;
34 #define KJV_REF_SEARCH 1
35 #define KJV_REF_EXACT 2
36 #define KJV_REF_EXACT_SET 3
37 #define KJV_REF_RANGE 4
38 #define KJV_REF_RANGE_EXT 5
40 typedef struct kjv_ref {
41 int type;
42 char book[64];
43 unsigned int chapter;
44 unsigned int chapter_end;
45 unsigned int verse;
46 unsigned int verse_end;
47 intset *verse_set;
48 char *search_str;
49 regex_t search;
50 } kjv_ref;
52 static kjv_ref *
53 kjv_newref()
55 return calloc(1, sizeof(kjv_ref));
58 static void
59 kjv_freeref(kjv_ref *ref)
61 if (ref) {
62 free(ref->search_str);
63 regfree(&ref->search);
64 free(ref);
68 static int
69 kjv_parseref(kjv_ref *ref, const char *ref_str)
71 // 1. <book>
72 // 2. <book>:?<chapter>
73 // 3. <book>:?<chapter>:<verse>
74 // 3a. <book>:?<chapter>:<verse>[,<verse>]...
75 // 4. <book>:?<chapter>-<chapter>
76 // 5. <book>:?<chapter>:<verse>-<verse>
77 // 6. <book>:?<chapter>:<verse>-<chapter>:<verse>
78 // 7. /<search>
79 // 8. <book>/search
80 // 9. <book>:?<chapter>/search
82 ref->type = 0;
83 ref->book[0] = '\0';
84 ref->chapter = 0;
85 ref->chapter_end = 0;
86 ref->verse = 0;
87 ref->verse_end = 0;
88 intset_free(ref->verse_set);
89 ref->verse_set = NULL;
90 free(ref->search_str);
91 ref->search_str = NULL;
92 regfree(&ref->search);
94 int n = 0;
96 sscanf(ref_str, "%*1[1-3]%n", &n);
97 bool has_booknum = n > 0;
98 if (has_booknum && sscanf(ref_str, "%1[1-3]%62[a-zA-Z ]%n", &ref->book[0], &ref->book[1], &n) == 2) {
99 ref_str = &ref_str[n];
100 } else if (!has_booknum && sscanf(ref_str, "%63[a-zA-Z ]%n", &ref->book[0], &n) == 1) {
101 // 1, 2, 3, 3a, 4, 5, 6, 8, 9
102 ref_str = &ref_str[n];
103 } else if (ref_str[0] == '/') {
104 // 7
105 goto search;
106 } else {
107 return 1;
110 if (sscanf(ref_str, ":%u%n", &ref->chapter, &n) == 1 || sscanf(ref_str, "%u%n", &ref->chapter, &n) == 1) {
111 // 2, 3, 3a, 4, 5, 6, 9
112 ref_str = &ref_str[n];
113 } else if (ref_str[0] == '/') {
114 // 8
115 goto search;
116 } else if (ref_str[0] == '\0') {
117 // 1
118 ref->type = KJV_REF_EXACT;
119 return 0;
120 } else {
121 return 1;
124 if (sscanf(ref_str, ":%u%n", &ref->verse, &n) == 1) {
125 // 3, 3a, 5, 6
126 ref_str = &ref_str[n];
127 } else if (sscanf(ref_str, "-%u%n", &ref->chapter_end, &n) == 1) {
128 // 4
129 if (ref_str[n] != '\0') {
130 return 1;
132 ref->type = KJV_REF_RANGE;
133 return 0;
134 } else if (ref_str[0] == '/') {
135 // 9
136 goto search;
137 } else if (ref_str[0] == '\0') {
138 // 2
139 ref->type = KJV_REF_EXACT;
140 return 0;
141 } else {
142 return 1;
145 unsigned int value;
146 int ret = sscanf(ref_str, "-%u%n", &value, &n);
147 if (ret == 1 && ref_str[n] == '\0') {
148 // 5
149 ref->verse_end = value;
150 ref->type = KJV_REF_RANGE;
151 return 0;
152 } else if (ret == 1) {
153 // 6
154 ref->chapter_end = value;
155 ref_str = &ref_str[n];
156 } else if (ref_str[0] == '\0') {
157 // 3
158 ref->type = KJV_REF_EXACT;
159 return 0;
160 } else if (sscanf(ref_str, ",%u%n", &value, &n) == 1) {
161 // 3a
162 ref->verse_set = intset_new();
163 intset_add(ref->verse_set, ref->verse);
164 intset_add(ref->verse_set, value);
165 ref_str = &ref_str[n];
166 while (true) {
167 if (sscanf(ref_str, ",%u%n", &value, &n) != 1) {
168 break;
170 intset_add(ref->verse_set, value);
171 ref_str = &ref_str[n];
173 if (ref_str[0] != '\0') {
174 return 1;
176 ref->type = KJV_REF_EXACT_SET;
177 return 0;
178 } else {
179 return 1;
182 if (sscanf(ref_str, ":%u%n", &ref->verse_end, &n) == 1 && ref_str[n] == '\0') {
183 // 6
184 ref->type = KJV_REF_RANGE_EXT;
185 return 0;
186 } else {
187 return 1;
190 search:
191 ref->type = KJV_REF_SEARCH;
192 if (regcomp(&ref->search, &ref_str[1], REG_EXTENDED|REG_ICASE|REG_NOSUB) != 0) {
193 return 2;
195 ref->search_str = strdup(&ref_str[1]);
196 return 0;
199 static char *
200 str_join(size_t n, char *strs[])
202 size_t length = 0;
203 for (size_t i = 0; i < n; i++) {
204 if (i > 0) {
205 length++;
207 length += strlen(strs[i]);
209 char *str = malloc(length + 1);
210 str[0] = '\0';
211 for (size_t i = 0; i < n; i++) {
212 if (i > 0) {
213 strcat(str, " ");
215 strcat(str, strs[i]);
217 return str;
220 static bool
221 kjv_bookequal(const char *a, const char *b, bool short_match)
223 for (size_t i = 0, j = 0; ; ) {
224 if ((!a[i] && !b[j]) || (short_match && !b[j])) {
225 return true;
226 } else if (a[i] == ' ') {
227 i++;
228 } else if (b[j] == ' ') {
229 j++;
230 } else if (tolower(a[i]) != tolower(b[j])) {
231 return false;
232 } else {
233 i++;
234 j++;
239 static bool
240 kjv_book_matches(const char *book, const kjv_verse *verse)
242 return kjv_bookequal(verse->book_name, book, false) ||
243 kjv_bookequal(verse->book_abbr, book, false) ||
244 kjv_bookequal(verse->book_name, book, true);
247 static bool
248 kjv_verse_matches(const kjv_ref *ref, const kjv_verse *verse)
250 switch (ref->type) {
251 case KJV_REF_SEARCH:
252 return (ref->book[0] == '\0' || kjv_book_matches(ref->book, verse)) &&
253 (ref->chapter == 0 || verse->chapter == ref->chapter) &&
254 regexec(&ref->search, verse->text, 0, NULL, 0) == 0;
256 case KJV_REF_EXACT:
257 return kjv_book_matches(ref->book, verse) &&
258 (ref->chapter == 0 || ref->chapter == verse->chapter) &&
259 (ref->verse == 0 || ref->verse == verse->verse);
261 case KJV_REF_EXACT_SET:
262 return kjv_book_matches(ref->book, verse) &&
263 (ref->chapter == 0 || verse->chapter == ref->chapter) &&
264 intset_contains(ref->verse_set, verse->verse);
266 case KJV_REF_RANGE:
267 return kjv_book_matches(ref->book, verse) &&
268 ((ref->chapter_end == 0 && ref->chapter == verse->chapter) ||
269 (verse->chapter >= ref->chapter && verse->chapter <= ref->chapter_end)) &&
270 (ref->verse == 0 || verse->verse >= ref->verse) &&
271 (ref->verse_end == 0 || verse->verse <= ref->verse_end);
273 case KJV_REF_RANGE_EXT:
274 return kjv_book_matches(ref->book, verse) &&
276 (verse->chapter == ref->chapter && verse->verse >= ref->verse && ref->chapter != ref->chapter_end) ||
277 (verse->chapter > ref->chapter && verse->chapter < ref->chapter_end) ||
278 (verse->chapter == ref->chapter_end && verse->verse <= ref->verse_end && ref->chapter != ref->chapter_end) ||
279 (ref->chapter == ref->chapter_end && verse->chapter == ref->chapter && verse->verse >= ref->verse && verse->verse <= ref->verse_end)
282 default:
283 return false;
287 #define KJV_DIRECTION_BEFORE -1
288 #define KJV_DIRECTION_AFTER 1
290 static int
291 kjv_chapter_bounds(int i, int direction, int maximum_steps)
293 assert(direction == -1 || direction == 1);
295 int steps = 0;
296 for ( ; 0 <= i && i < kjv_verses_length; i += direction) {
297 if (maximum_steps != -1 && steps >= maximum_steps) {
298 break;
300 if ((direction == -1 && i == 0) || (direction == 1 && i + 1 == kjv_verses_length)) {
301 break;
303 const kjv_verse *current = &kjv_verses[i], *next = &kjv_verses[i + direction];
304 if (current->book != next->book || current->chapter != next->chapter) {
305 break;
307 steps++;
309 return i;
312 static int
313 kjv_next_match(const kjv_ref *ref, int i)
315 for ( ; i < kjv_verses_length; i++) {
316 const kjv_verse *verse = &kjv_verses[i];
317 if (kjv_verse_matches(ref, verse)) {
318 return i;
321 return -1;
324 typedef struct {
325 int start;
326 int end;
327 } kjv_range;
329 typedef struct {
330 int current;
331 int next_match;
332 kjv_range matches[2];
333 } kjv_next_data;
335 static void
336 kjv_next_addrange(kjv_next_data *next, kjv_range range) {
337 if (next->matches[0].start == -1 && next->matches[0].end == -1) {
338 next->matches[0] = range;
339 } else if (range.start < next->matches[0].end) {
340 next->matches[0] = range;
341 } else {
342 next->matches[1] = range;
346 static int
347 kjv_next_verse(const kjv_ref *ref, const kjv_config *config, kjv_next_data *next)
349 if (next->current >= kjv_verses_length) {
350 return -1;
353 if (next->matches[0].start != -1 && next->matches[0].end != -1 && next->current >= next->matches[0].end) {
354 next->matches[0] = next->matches[1];
355 next->matches[1] = (kjv_range){-1, -1};
358 if ((next->next_match == -1 || next->next_match < next->current) && next->next_match < kjv_verses_length) {
359 int next_match = kjv_next_match(ref, next->current);
360 if (next_match >= 0) {
361 next->next_match = next_match;
362 kjv_range bounds = {
363 .start = kjv_chapter_bounds(next_match, KJV_DIRECTION_BEFORE, config->context_chapter ? -1 : config->context_before),
364 .end = kjv_chapter_bounds(next_match, KJV_DIRECTION_AFTER, config->context_chapter ? -1 : config->context_after) + 1,
366 kjv_next_addrange(next, bounds);
367 } else {
368 next_match = kjv_verses_length;
372 if (next->matches[0].start == -1 && next->matches[0].end == -1) {
373 return -1;
376 if (next->current < next->matches[0].start) {
377 next->current = next->matches[0].start;
380 return next->current++;
383 #define ESC_BOLD "\033[1m"
384 #define ESC_UNDERLINE "\033[4m"
385 #define ESC_RESET "\033[m"
387 static void
388 kjv_output_verse(const kjv_verse *verse, FILE *f, const kjv_config *config, const kjv_verse *last_printed)
390 if (last_printed == NULL || verse->book != last_printed->book) {
391 fprintf(f, ESC_UNDERLINE "%s" ESC_RESET "\n", verse->book_name);
393 fprintf(f, ESC_BOLD "%d:%d" ESC_RESET "\t", verse->chapter, verse->verse);
394 char verse_text[1024];
395 strcpy(verse_text, verse->text);
396 size_t characters_printed = 0;
397 char *word = strtok(verse_text, " ");
398 while (word != NULL) {
399 size_t word_length = strlen(word);
400 if (config->linewrap && characters_printed + word_length + (characters_printed > 0 ? 1 : 0) > config->maximum_line_length - 8 - 2) {
401 fprintf(f, "\n\t");
402 characters_printed = 0;
404 if (characters_printed > 0) {
405 fprintf(f, " ");
406 characters_printed++;
408 fprintf(f, "%s", word);
409 characters_printed += word_length;
410 word = strtok(NULL, " ");
412 fprintf(f, "\n");
415 static bool
416 kjv_output(const kjv_ref *ref, FILE *f, const kjv_config *config)
418 kjv_next_data next = {
419 .current = 0,
420 .next_match = -1,
421 .matches = {
422 {-1, -1},
423 {-1, -1},
427 kjv_verse *last_printed = NULL;
428 for (int verse_id; (verse_id = kjv_next_verse(ref, config, &next)) != -1; ) {
429 kjv_verse *verse = &kjv_verses[verse_id];
430 kjv_output_verse(verse, f, config, last_printed);
431 last_printed = verse;
433 return last_printed != NULL;
436 static int
437 kjv_render(const kjv_ref *ref, const kjv_config *config)
439 int fds[2];
440 if (pipe(fds) == -1) {
441 return 1;
444 pid_t pid = fork();
445 if (pid == 0) {
446 close(fds[1]);
447 dup2(fds[0], STDIN_FILENO);
448 char *args[9] = {NULL};
449 int arg = 0;
450 args[arg++] = "less";
451 args[arg++] = "-J";
452 args[arg++] = "-I";
453 if (ref->search_str != NULL) {
454 args[arg++] = "-p";
455 args[arg++] = ref->search_str;
457 args[arg++] = "-R";
458 args[arg++] = "-f";
459 args[arg++] = "-";
460 args[arg++] = NULL;
461 execvp("less", args);
462 printf("unable not exec less\n");
463 _exit(0);
464 } else if (pid == -1) {
465 printf("unable to fork\n");
466 return 2;
468 close(fds[0]);
469 FILE *output = fdopen(fds[1], "w");
470 bool printed = kjv_output(ref, output, config);
471 if (!printed) {
472 kill(pid, SIGTERM);
474 fclose(output);
475 waitpid(pid, NULL, 0);
476 if (!printed) {
477 printf("unknown reference\n");
479 return 0;
482 const char *
483 usage = "usage: kjv [flags] [reference...]\n"
484 "\n"
485 "Flags:\n"
486 " -A num number of verses of context after matching verses\n"
487 " -B num number of verses of context before matching verses\n"
488 " -C show matching verses in context of the chapter\n"
489 " -l list books\n"
490 " -W no line wrap\n"
491 " -h show help\n"
492 "\n"
493 "Reference:\n"
494 " <Book>\n"
495 " Individual book\n"
496 " <Book>:<Chapter>\n"
497 " Individual chapter of a book\n"
498 " <Book>:<Chapter>:<Verse>[,<Verse>]...\n"
499 " Individual verse(s) of a specific chapter of a book\n"
500 " <Book>:<Chapter>-<Chapter>\n"
501 " Range of chapters in a book\n"
502 " <Book>:<Chapter>:<Verse>-<Verse>\n"
503 " Range of verses in a book chapter\n"
504 " <Book>:<Chapter>:<Verse>-<Chapter>:<Verse>\n"
505 " Range of chapters and verses in a book\n"
506 "\n"
507 " /<Search>\n"
508 " All verses that match a pattern\n"
509 " <Book>/<Search>\n"
510 " All verses in a book that match a pattern\n"
511 " <Book>:<Chapter>/<Search>\n"
512 " All verses in a chapter of a book that match a pattern\n";
515 main(int argc, char *argv[])
517 kjv_config config = {
518 .linewrap = true,
519 .maximum_line_length = 80,
521 .context_before = 0,
522 .context_after = 0,
523 .context_chapter = false,
526 bool list_books = false;
528 opterr = 0;
529 for (int opt; (opt = getopt(argc, argv, "A:B:ClWh")) != -1; ) {
530 char *endptr;
531 switch (opt) {
532 case 'A':
533 config.context_after = strtol(optarg, &endptr, 10);
534 if (endptr[0] != '\0') {
535 fprintf(stderr, "kjv: invalid flag value for -A\n\n%s", usage);
536 return 1;
538 break;
539 case 'B':
540 config.context_before = strtol(optarg, &endptr, 10);
541 if (endptr[0] != '\0') {
542 fprintf(stderr, "kjv: invalid flag value for -B\n\n%s", usage);
543 return 1;
545 break;
546 case 'C':
547 config.context_chapter = true;
548 break;
549 case 'l':
550 list_books = true;
551 break;
552 case 'W':
553 config.linewrap = false;
554 break;
555 case 'h':
556 printf("%s", usage);
557 return 0;
558 case '?':
559 fprintf(stderr, "kjv: invalid flag -%c\n\n%s", optopt, usage);
560 return 1;
564 if (list_books) {
565 char *last_book_printed = NULL;
566 for (int i = 0; i < kjv_verses_length; i++) {
567 if (last_book_printed == NULL || strcmp(kjv_verses[i].book_name, last_book_printed) != 0) {
568 printf("%s (%s)\n", kjv_verses[i].book_name, kjv_verses[i].book_abbr);
569 last_book_printed = kjv_verses[i].book_name;
572 return 0;
575 struct winsize ttysize;
576 if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ttysize) == 0 && ttysize.ws_col > 0) {
577 config.maximum_line_length = ttysize.ws_col;
580 signal(SIGPIPE, SIG_IGN);
582 if (argc == optind) {
583 using_history();
584 while (true) {
585 char *input = readline("kjv> ");
586 if (input == NULL) {
587 break;
589 add_history(input);
590 kjv_ref *ref = kjv_newref();
591 int success = kjv_parseref(ref, input);
592 free(input);
593 if (success == 0) {
594 kjv_render(ref, &config);
596 kjv_freeref(ref);
598 } else {
599 char *ref_str = str_join(argc-optind, &argv[optind]);
600 kjv_ref *ref = kjv_newref();
601 int success = kjv_parseref(ref, ref_str);
602 free(ref_str);
603 if (success == 0) {
604 kjv_render(ref, &config);
606 kjv_freeref(ref);
609 return 0;