esv/esv.d

307 lines
7.2 KiB
D

/*
* esv: read the Bible from your terminal
*
* The GPLv2 License (GPLv2)
* Copyright (c) 2023-2024 Jeremy Baxter
*
* esv is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* esv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with esv. If not, see <http://www.gnu.org/licenses/>.
*/
module esv;
import std.algorithm : startsWith;
import std.conv : to, ConvException;
import std.file : exists, mkdirRecurse, write, FileException;
import std.format : format;
import std.getopt : getopt, GetOptException;
import std.path : baseName, dirName, expandTilde, isValidPath;
import std.process : environment, executeShell;
import std.stdio : writef, writeln, writefln, File;
import std.string : splitLines;
import esvapi;
import initial;
import cf = config;
@safe:
bool aFlag; /* audio */
string CFlag; /* config */
bool fFlag, FFlag; /* footnotes */
bool hFlag, HFlag; /* headings */
int lFlag; /* line length */
bool lFlagSpecified;
bool nFlag, NFlag; /* verse numbers */
bool rFlag, RFlag; /* passage references */
string sFlag; /* search passages */
bool VFlag; /* show version */
string[] mainArgs;
File stderr;
version (OpenBSD) {
immutable(char) *promises;
}
int
main(string[] args)
{
string apiKey;
string configPath;
INIUnit ini;
ESVApi esv;
mainArgs = args;
version (OpenBSD) () @trusted {
import core.sys.openbsd.unistd : pledge;
import std.string : toStringz;
promises = toStringz("stdio rpath wpath cpath inet dns tty proc exec prot_exec");
pledge(promises, null);
}();
/* @safe way of opening stderr on Unix */
stderr = File("/dev/stderr", "w");
/* Parse command-line options */
try {
import std.getopt : config;
getopt(args,
config.bundling,
config.caseSensitive,
"a", &aFlag,
"C", &CFlag,
"F", &FFlag, "f", &fFlag,
"H", &HFlag, "h", &hFlag,
"l", &onLineLength,
"N", &NFlag, "n", &nFlag,
"R", &RFlag, "r", &rFlag,
"s", &sFlag,
"V", &VFlag,
);
} catch (GetOptException e) {
enforceDie(!e.msg.startsWith("Unrecognized option"),
"unknown option " ~ e.extractOpt());
enforceDie(!e.msg.startsWith("Missing value for argument"),
"missing argument for option " ~ e.extractOpt());
die(e.msg); /* catch-all */
}
if (VFlag) {
writeln("esv " ~ cf.esvVersion);
return 0;
}
if (sFlag != "") {
/* skip argument validation */
goto config;
}
if (args.length < 3) {
stderr.writefln("usage: %s [-aFfHhNnRrV] [-C config] [-l length] [-s query] book verses", args[0].baseName());
return 1;
}
enforceDie(bookValid(args[1].parseBook()),
"book '%s' does not exist", args[1]);
enforceDie(verseValid(args[2]),
"invalid verse format '%s'", args[2]);
/* determine configuration file
* Options have first priority, then environment variables,
* then the default path */
config:
configPath = environment.get(cf.configEnv, cf.configPath).expandTilde();
try {
if (CFlag != "") { /* if -C was given */
enforceDie(isValidPath(CFlag), CFlag ~ ": invalid path");
configPath = CFlag.expandTilde();
} else {
enforceDie(isValidPath(configPath),
configPath ~ ": invalid path");
if (!configPath.exists()) {
mkdirRecurse(configPath.dirName());
configPath.write(format!
"## Configuration file for esv.
# An API key is required to access the ESV Bible API.
[api]
key = %s
# Settings that modify how passages are displayed:
[passage]
#footnotes = false
#headings = false
#passage-references = false
#verse-numbers = false
"(cf.apiKey));
}
}
readINIFile(ini, configPath);
} catch (FileException e) {
/* filesystem syscall errors */
die(e.msg);
}
enforceDie(!(aFlag && sFlag), "cannot specify both -a and -s flags");
apiKey = ini["api"].key("key");
enforceDie(apiKey != null,
"API key not present in configuration file; cannot proceed");
esv = new ESVApi(apiKey);
if (aFlag) {
string tmpf, player;
try
tmpf = esv.getAudioPassage(args[1], args[2]);
catch (CurlException e)
die(e.msg);
player = environment.get(cf.playerEnv, cf.mp3Player);
/* check for an audio player */
enforceDie(
executeShell(
format!"command -v %s >/dev/null 2>&1"(player)
).status == 0,
player ~ " is required for audio mode; cannot continue");
/* esv has built-in support for mpg123 and mpv.
* other players will work, just recompile with
* the DEFAULT_MPEGPLAYER enum set differently
* or use the ESV_PLAYER environment variable */
player ~=
player == "mpg123" ? " -q " :
player == "mpv" ? " --msg-level=all=no " : " ";
/* spawn the player */
executeShell(player ~ tmpf);
return 0;
}
if (sFlag) {
try
writeln(esv.searchFormat(sFlag));
catch (ESVException)
die("no results for search");
catch (CurlException e)
die(e.msg);
return 0;
}
esv.extraParameters = ini["api"].key("parameters", "");
/* Get [passage] keys */
foreach (string key; [
"footnotes",
"headings",
"passage-references",
"verse-numbers"
]) {
try
esv.opts.b["include-" ~ key] =
ini["passage"].keyAs!bool(key, true);
catch (INITypeException e)
die(configPath ~ ": " ~ e.msg);
}
/* Get line-length ([passage]) */
try
esv.opts.i["line-length"] =
lFlagSpecified ? lFlag :
ini["passage"].keyAs!int("line-length", terminalColumns());
catch (INITypeException e)
die(configPath ~ ": " ~ e.msg);
if (fFlag) esv.opts.b["include-footnotes"] = true;
if (hFlag) esv.opts.b["include-headings"] = true;
if (nFlag) esv.opts.b["include-verse-numbers"] = true;
if (rFlag) esv.opts.b["include-passage-references"] = true;
if (FFlag) esv.opts.b["include-footnotes"] = false;
if (HFlag) esv.opts.b["include-headings"] = false;
if (NFlag) esv.opts.b["include-verse-numbers"] = false;
if (RFlag) esv.opts.b["include-passage-references"] = false;
try
writeln(esv.getPassage(args[1].parseBook(), args[2]));
catch (CurlException e)
die(e.msg);
return 0;
}
private void
warn(string mesg)
{
stderr.writeln(baseName(mainArgs[0]) ~ ": " ~ mesg);
}
private void
die(string mesg) @trusted
{
import core.runtime : Runtime;
import core.stdc.stdlib : exit;
warn(mesg);
Runtime.terminate();
exit(1);
}
private ushort
terminalColumns() @trusted
{
import core.sys.posix.sys.ioctl;
winsize w;
ioctl(1, TIOCGWINSZ, &w);
return w.ws_col > 72 ? 72 : w.ws_col;
}
private void
enforceDie(A...)(bool cond, string fmt, A a)
{
import std.format : format;
if (!cond)
die(format(fmt, a));
}
private string
extractOpt(in GetOptException e)
{
import std.regex : matchFirst;
return e.msg.matchFirst("-.")[0];
}
private void
onLineLength(string flag, string value)
{
lFlagSpecified = true;
try
lFlag = value.to!int();
catch (ConvException e)
die("illegal argument to -l option -- must be a positive integer");
}
private string
parseBook(in string book)
{
import std.string : tr;
return book.tr("-_", " ");
}