/* * esv: read the Bible from your terminal * * The GPLv2 License (GPLv2) * Copyright (c) 2023-2025 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 . */ module esv; 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; import std.process : environment, executeShell; import std.stdio : writeln, writefln; import std.string : splitLines; import esvapi; import initial; import util; import cf = config; @safe: bool aFlag; /* audio */ string cFlag; /* config path */ bool fFlag, FFlag; /* footnotes */ bool hFlag, HFlag; /* headings */ int lFlag; /* line length */ bool lFlagSpecified; bool nFlag, NFlag; /* verse numbers */ bool rFlag, RFlag; /* passage references */ bool VFlag; /* show version */ int main(string[] args) { INIUnit ini; ESVApi esv; string apiKey; string configPath; sharedInit(args); cFlag = null; /* 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, "V", &VFlag, ); } catch (GetOptException e) { handleOptError(e.msg); } if (VFlag) { writeln("esv " ~ cf.esvVersion); return 0; } if (args.length < 3) { stderr.writefln( "usage: %s [-aFfHhNnRrV] [-c config] [-l length] book verses", baseName(args[0])); return 1; } enforceDie(bookValid(parseBook(args[1])), "book '%s' does not exist", args[1]); enforceDie(verseValid(args[2]), "invalid verse format '%s'", args[2]); /* determine configuration file: options take first priority, * then environment variables, and then the default path */ config: configPath = environment.get(cf.configEnv, cf.configPath) .expandTilde(); try { if (cFlag) configPath = cFlag.expandTilde(); if (!configPath.exists()) { mkdirRecurse(dirName(configPath)); 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); } apiKey = ini["api"].key("key"); enforceDie(apiKey != null, "API key not present in configuration file; cannot proceed"); esv = ESVApi(apiKey); if (aFlag) { string tmpf, player; try tmpf = esv.getAudioPassage(parseBook(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 set ESV_PLAYER */ player ~= player == "mpg123" ? " -q " : player == "mpv" ? " --msg-level=all=no " : " "; /* spawn the player */ executeShell(player ~ tmpf); 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(parseBook(args[1]), args[2])); catch (CurlException e) die(e.msg); return 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"); }