2 kjv: Read the Word of God from your terminal
14 #include <readline/readline.h>
15 #include <readline/history.h>
17 #include <sys/types.h>
20 #include <sys/ioctl.h>
26 int maximum_line_length
;
33 #define KJV_REF_SEARCH 1
34 #define KJV_REF_EXACT 2
35 #define KJV_REF_EXACT_SET 3
36 #define KJV_REF_RANGE 4
37 #define KJV_REF_RANGE_EXT 5
39 typedef struct kjv_ref
{
43 unsigned int chapter_end
;
45 unsigned int verse_end
;
54 return calloc(1, sizeof(kjv_ref
));
58 kjv_freeref(kjv_ref
*ref
)
61 free(ref
->search_str
);
62 regfree(&ref
->search
);
69 kjv_bookequal(const char *a
, const char *b
, bool short_match
)
71 for (size_t i
= 0, j
= 0; ; ) {
72 if ((!a
[i
] && !b
[j
]) || (short_match
&& !b
[j
])) {
74 } else if (a
[i
] == ' ') {
76 } else if (b
[j
] == ' ') {
78 } else if (tolower(a
[i
]) != tolower(b
[j
])) {
88 kjv_book_matches(const kjv_book
*book
, const char *s
)
90 return kjv_bookequal(book
->name
, s
, false) ||
91 kjv_bookequal(book
->abbr
, s
, false) ||
92 kjv_bookequal(book
->name
, s
, true);
96 kjv_book_fromname(const char *s
)
98 for (int i
= 0; i
< kjv_books_length
; i
++) {
99 const kjv_book
*book
= &kjv_books
[i
];
100 if (kjv_book_matches(book
, s
)) {
108 kjv_scanbook(const char *s
, int *n
)
112 for (i
= 0; s
[i
]; i
++) {
115 } else if (('a' <= s
[i
] && s
[i
] <= 'z') || ('A' <= s
[i
] && s
[i
] <= 'Z')) {
117 } else if ('0' <= s
[i
] && s
[i
] <= '9' && mode
== 0) {
128 kjv_parseref(kjv_ref
*ref
, const char *ref_str
)
131 // 2. <book>:?<chapter>
132 // 3. <book>:?<chapter>:<verse>
133 // 3a. <book>:?<chapter>:<verse>[,<verse>]...
134 // 4. <book>:?<chapter>-<chapter>
135 // 5. <book>:?<chapter>:<verse>-<verse>
136 // 6. <book>:?<chapter>:<verse>-<chapter>:<verse>
139 // 9. <book>:?<chapter>/search
144 ref
->chapter_end
= 0;
147 intset_free(ref
->verse_set
);
148 ref
->verse_set
= NULL
;
149 free(ref
->search_str
);
150 ref
->search_str
= NULL
;
151 regfree(&ref
->search
);
154 if (kjv_scanbook(ref_str
, &n
) == 1) {
155 // 1, 2, 3, 3a, 4, 5, 6, 8, 9
156 char *bookname
= strndup(ref_str
, n
);
157 ref
->book
= kjv_book_fromname(bookname
);
159 ref_str
= &ref_str
[n
];
160 } else if (ref_str
[0] == '/') {
167 if (sscanf(ref_str
, ":%u%n", &ref
->chapter
, &n
) == 1 || sscanf(ref_str
, "%u%n", &ref
->chapter
, &n
) == 1) {
168 // 2, 3, 3a, 4, 5, 6, 9
169 ref_str
= &ref_str
[n
];
170 } else if (ref_str
[0] == '/') {
173 } else if (ref_str
[0] == '\0') {
175 ref
->type
= KJV_REF_EXACT
;
181 if (sscanf(ref_str
, ":%u%n", &ref
->verse
, &n
) == 1) {
183 ref_str
= &ref_str
[n
];
184 } else if (sscanf(ref_str
, "-%u%n", &ref
->chapter_end
, &n
) == 1) {
186 if (ref_str
[n
] != '\0') {
189 ref
->type
= KJV_REF_RANGE
;
191 } else if (ref_str
[0] == '/') {
194 } else if (ref_str
[0] == '\0') {
196 ref
->type
= KJV_REF_EXACT
;
203 int ret
= sscanf(ref_str
, "-%u%n", &value
, &n
);
204 if (ret
== 1 && ref_str
[n
] == '\0') {
206 ref
->verse_end
= value
;
207 ref
->type
= KJV_REF_RANGE
;
209 } else if (ret
== 1) {
211 ref
->chapter_end
= value
;
212 ref_str
= &ref_str
[n
];
213 } else if (ref_str
[0] == '\0') {
215 ref
->type
= KJV_REF_EXACT
;
217 } else if (sscanf(ref_str
, ",%u%n", &value
, &n
) == 1) {
219 ref
->verse_set
= intset_new();
220 intset_add(ref
->verse_set
, ref
->verse
);
221 intset_add(ref
->verse_set
, value
);
222 ref_str
= &ref_str
[n
];
224 if (sscanf(ref_str
, ",%u%n", &value
, &n
) != 1) {
227 intset_add(ref
->verse_set
, value
);
228 ref_str
= &ref_str
[n
];
230 if (ref_str
[0] != '\0') {
233 ref
->type
= KJV_REF_EXACT_SET
;
239 if (sscanf(ref_str
, ":%u%n", &ref
->verse_end
, &n
) == 1 && ref_str
[n
] == '\0') {
241 ref
->type
= KJV_REF_RANGE_EXT
;
248 ref
->type
= KJV_REF_SEARCH
;
249 if (regcomp(&ref
->search
, &ref_str
[1], REG_EXTENDED
|REG_ICASE
|REG_NOSUB
) != 0) {
252 ref
->search_str
= strdup(&ref_str
[1]);
257 str_join(size_t n
, char *strs
[])
260 for (size_t i
= 0; i
< n
; i
++) {
264 length
+= strlen(strs
[i
]);
266 char *str
= malloc(length
+ 1);
268 for (size_t i
= 0; i
< n
; i
++) {
272 strcat(str
, strs
[i
]);
278 kjv_verse_matches(const kjv_ref
*ref
, const kjv_verse
*verse
)
282 return (ref
->book
== 0 || ref
->book
== verse
->book
) &&
283 (ref
->chapter
== 0 || verse
->chapter
== ref
->chapter
) &&
284 regexec(&ref
->search
, verse
->text
, 0, NULL
, 0) == 0;
287 return ref
->book
== verse
->book
&&
288 (ref
->chapter
== 0 || ref
->chapter
== verse
->chapter
) &&
289 (ref
->verse
== 0 || ref
->verse
== verse
->verse
);
291 case KJV_REF_EXACT_SET
:
292 return ref
->book
== verse
->book
&&
293 (ref
->chapter
== 0 || verse
->chapter
== ref
->chapter
) &&
294 intset_contains(ref
->verse_set
, verse
->verse
);
297 return ref
->book
== verse
->book
&&
298 ((ref
->chapter_end
== 0 && ref
->chapter
== verse
->chapter
) ||
299 (verse
->chapter
>= ref
->chapter
&& verse
->chapter
<= ref
->chapter_end
)) &&
300 (ref
->verse
== 0 || verse
->verse
>= ref
->verse
) &&
301 (ref
->verse_end
== 0 || verse
->verse
<= ref
->verse_end
);
303 case KJV_REF_RANGE_EXT
:
304 return ref
->book
== verse
->book
&&
306 (verse
->chapter
== ref
->chapter
&& verse
->verse
>= ref
->verse
&& ref
->chapter
!= ref
->chapter_end
) ||
307 (verse
->chapter
> ref
->chapter
&& verse
->chapter
< ref
->chapter_end
) ||
308 (verse
->chapter
== ref
->chapter_end
&& verse
->verse
<= ref
->verse_end
&& ref
->chapter
!= ref
->chapter_end
) ||
309 (ref
->chapter
== ref
->chapter_end
&& verse
->chapter
== ref
->chapter
&& verse
->verse
>= ref
->verse
&& verse
->verse
<= ref
->verse_end
)
317 #define KJV_DIRECTION_BEFORE -1
318 #define KJV_DIRECTION_AFTER 1
321 kjv_chapter_bounds(int i
, int direction
, int maximum_steps
)
323 assert(direction
== -1 || direction
== 1);
326 for ( ; 0 <= i
&& i
< kjv_verses_length
; i
+= direction
) {
327 if (maximum_steps
!= -1 && steps
>= maximum_steps
) {
330 if ((direction
== -1 && i
== 0) || (direction
== 1 && i
+ 1 == kjv_verses_length
)) {
333 const kjv_verse
*current
= &kjv_verses
[i
], *next
= &kjv_verses
[i
+ direction
];
334 if (current
->book
!= next
->book
|| current
->chapter
!= next
->chapter
) {
343 kjv_next_match(const kjv_ref
*ref
, int i
)
345 for ( ; i
< kjv_verses_length
; i
++) {
346 const kjv_verse
*verse
= &kjv_verses
[i
];
347 if (kjv_verse_matches(ref
, verse
)) {
362 kjv_range matches
[2];
366 kjv_next_addrange(kjv_next_data
*next
, kjv_range range
) {
367 if (next
->matches
[0].start
== -1 && next
->matches
[0].end
== -1) {
368 next
->matches
[0] = range
;
369 } else if (range
.start
< next
->matches
[0].end
) {
370 next
->matches
[0] = range
;
372 next
->matches
[1] = range
;
377 kjv_next_verse(const kjv_ref
*ref
, const kjv_config
*config
, kjv_next_data
*next
)
379 if (next
->current
>= kjv_verses_length
) {
383 if (next
->matches
[0].start
!= -1 && next
->matches
[0].end
!= -1 && next
->current
>= next
->matches
[0].end
) {
384 next
->matches
[0] = next
->matches
[1];
385 next
->matches
[1] = (kjv_range
){-1, -1};
388 if ((next
->next_match
== -1 || next
->next_match
< next
->current
) && next
->next_match
< kjv_verses_length
) {
389 int next_match
= kjv_next_match(ref
, next
->current
);
390 if (next_match
>= 0) {
391 next
->next_match
= next_match
;
393 .start
= kjv_chapter_bounds(next_match
, KJV_DIRECTION_BEFORE
, config
->context_chapter
? -1 : config
->context_before
),
394 .end
= kjv_chapter_bounds(next_match
, KJV_DIRECTION_AFTER
, config
->context_chapter
? -1 : config
->context_after
) + 1,
396 kjv_next_addrange(next
, bounds
);
398 next_match
= kjv_verses_length
;
402 if (next
->matches
[0].start
== -1 && next
->matches
[0].end
== -1) {
406 if (next
->current
< next
->matches
[0].start
) {
407 next
->current
= next
->matches
[0].start
;
410 return next
->current
++;
413 #define ESC_BOLD "\033[1m"
414 #define ESC_UNDERLINE "\033[4m"
415 #define ESC_RESET "\033[m"
418 kjv_output_verse(const kjv_verse
*verse
, FILE *f
, const kjv_config
*config
)
420 fprintf(f
, ESC_BOLD
"%d:%d" ESC_RESET
"\t", verse
->chapter
, verse
->verse
);
421 char verse_text
[1024];
422 strcpy(verse_text
, verse
->text
);
423 size_t characters_printed
= 0;
424 char *word
= strtok(verse_text
, " ");
425 while (word
!= NULL
) {
426 size_t word_length
= strlen(word
);
427 if (characters_printed
+ word_length
+ (characters_printed
> 0 ? 1 : 0) > config
->maximum_line_length
- 8 - 2) {
429 characters_printed
= 0;
431 if (characters_printed
> 0) {
433 characters_printed
++;
435 fprintf(f
, "%s", word
);
436 characters_printed
+= word_length
;
437 word
= strtok(NULL
, " ");
443 kjv_output(const kjv_ref
*ref
, FILE *f
, const kjv_config
*config
)
445 kjv_next_data next
= {
454 kjv_verse
*last_printed
= NULL
;
455 for (int verse_id
; (verse_id
= kjv_next_verse(ref
, config
, &next
)) != -1; ) {
456 kjv_verse
*verse
= &kjv_verses
[verse_id
];
457 if (last_printed
== NULL
|| verse
->book
!= last_printed
->book
) {
458 if (last_printed
!= NULL
) {
461 fprintf(f
, ESC_UNDERLINE
"%s" ESC_RESET
"\n\n", kjv_books
[verse
->book
- 1].name
);
463 kjv_output_verse(verse
, f
, config
);
464 last_printed
= verse
;
466 return last_printed
!= NULL
;
470 kjv_render(const kjv_ref
*ref
, const kjv_config
*config
)
473 if (pipe(fds
) == -1) {
480 dup2(fds
[0], STDIN_FILENO
);
481 char *args
[9] = {NULL
};
483 args
[arg
++] = "less";
486 if (ref
->search_str
!= NULL
) {
488 args
[arg
++] = ref
->search_str
;
494 execvp("less", args
);
495 printf("unable not exec less\n");
497 } else if (pid
== -1) {
498 printf("unable to fork\n");
502 FILE *output
= fdopen(fds
[1], "w");
503 bool printed
= kjv_output(ref
, output
, config
);
508 waitpid(pid
, NULL
, 0);
510 printf("unknown reference\n");
516 usage
= "usage: kjv [flags] [reference...]\n"
519 " -A num number of verses of context after matching verses\n"
520 " -B num number of verses of context before matching verses\n"
521 " -C show matching verses in context of the chapter\n"
528 " <Book>:<Chapter>\n"
529 " Individual chapter of a book\n"
530 " <Book>:<Chapter>:<Verse>[,<Verse>]...\n"
531 " Individual verse(s) of a specific chapter of a book\n"
532 " <Book>:<Chapter>-<Chapter>\n"
533 " Range of chapters in a book\n"
534 " <Book>:<Chapter>:<Verse>-<Verse>\n"
535 " Range of verses in a book chapter\n"
536 " <Book>:<Chapter>:<Verse>-<Chapter>:<Verse>\n"
537 " Range of chapters and verses in a book\n"
540 " All verses that match a pattern\n"
542 " All verses in a book that match a pattern\n"
543 " <Book>:<Chapter>/<Search>\n"
544 " All verses in a chapter of a book that match a pattern\n";
547 main(int argc
, char *argv
[])
549 kjv_config config
= {
550 .maximum_line_length
= 80,
554 .context_chapter
= false,
557 bool list_books
= false;
560 for (int opt
; (opt
= getopt(argc
, argv
, "A:B:ClWh")) != -1; ) {
564 config
.context_after
= strtol(optarg
, &endptr
, 10);
565 if (endptr
[0] != '\0') {
566 fprintf(stderr
, "kjv: invalid flag value for -A\n\n%s", usage
);
571 config
.context_before
= strtol(optarg
, &endptr
, 10);
572 if (endptr
[0] != '\0') {
573 fprintf(stderr
, "kjv: invalid flag value for -B\n\n%s", usage
);
578 config
.context_chapter
= true;
587 fprintf(stderr
, "kjv: invalid flag -%c\n\n%s", optopt
, usage
);
593 for (int i
= 0; i
< kjv_books_length
; i
++) {
594 kjv_book
*book
= &kjv_books
[i
];
595 printf("%s (%s)\n", book
->name
, book
->abbr
);
600 struct winsize ttysize
;
601 if (ioctl(STDOUT_FILENO
, TIOCGWINSZ
, &ttysize
) == 0 && ttysize
.ws_col
> 0) {
602 config
.maximum_line_length
= ttysize
.ws_col
;
605 signal(SIGPIPE
, SIG_IGN
);
607 if (argc
== optind
) {
610 char *input
= readline("kjv> ");
615 kjv_ref
*ref
= kjv_newref();
616 int success
= kjv_parseref(ref
, input
);
619 kjv_render(ref
, &config
);
624 char *ref_str
= str_join(argc
-optind
, &argv
[optind
]);
625 kjv_ref
*ref
= kjv_newref();
626 int success
= kjv_parseref(ref
, ref_str
);
629 kjv_render(ref
, &config
);