From 2e4b067e492cf1ca0bab39e18881de1fc737b7bb Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Mon, 27 Mar 2023 12:02:37 +1300 Subject: [PATCH 001/133] Preserve dmd compatibility by removing one byte from the Makefile --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 7603231..5be1678 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ PREFIX = /usr/local MANPREFIX = /usr/share/man DC = ldc2 -CFLAGS = -Os -I${IMPORT} +CFLAGS = -O -I${IMPORT} OBJS = main.o esv.o ini.o ifeq (${DEBUG},) From 04aea47d808babe0131bf08390201eb9b0861fe3 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Mon, 27 Mar 2023 12:03:32 +1300 Subject: [PATCH 002/133] Rename BIBLE_BOOKS array to ESVAPI_BIBLE_BOOKS --- esv.d | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esv.d b/esv.d index 53303eb..13bdca1 100644 --- a/esv.d +++ b/esv.d @@ -45,7 +45,7 @@ import std.utf : toUTF8; import std.net.curl; const enum ESVAPI_URL = "https://api.esv.org/v3/passage"; -const string[] BIBLE_BOOKS = [ +const string[] ESVAPI_BIBLE_BOOKS = [ // Old Testament "Genesis", "Exodus", @@ -193,7 +193,7 @@ class EsvAPI */ final bool validateBook(const string book) const nothrow { - foreach (string b; BIBLE_BOOKS) + foreach (string b; ESVAPI_BIBLE_BOOKS) { if (book.capitalize() == b.capitalize()) return true; From 4992a8684c5d129bba9297e438d6258dd6d5a2d8 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Mon, 27 Mar 2023 12:46:35 +1300 Subject: [PATCH 003/133] De-hardcode mpg123 The MPEG player is no longer hard-coded as mpg123, and now customizable by changing the enum `DEFAULT_MPEGPLAYER`. --- main.d | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/main.d b/main.d index 71c7cdc..24c670b 100644 --- a/main.d +++ b/main.d @@ -33,6 +33,7 @@ import dini; enum VERSION = "0.1.0"; enum DEFAULT_CONFIGPATH = "~/.config/esv.conf"; +enum DEFAULT_MPEGPLAYER = "mpg123"; enum DEFAULT_PAGER = "less"; enum ENV_CONFIG = "ESV_CONFIG"; @@ -170,13 +171,23 @@ int main(string[] args) if (optAudio) { // check for mpg123 - if (executeShell("which mpg123 >/dev/null 2>&1").status > 0) { - panic("mpg123 is required for audio mode; cannot continue"); + if (executeShell("which " ~ DEFAULT_MPEGPLAYER ~ " >/dev/null 2>&1").status > 0) { + panic(DEFAULT_MPEGPLAYER ~ " is required for audio mode; cannot continue"); return 1; } else { string tmpf = esv.getAudioVerses(args[1], args[2]); + string mpegPlayer; + // esv has built-in support for mpg123 and mpv + // other players will work, just recompile with + // the DEFAULT_MPEGPLAYER enum set differently + if (DEFAULT_MPEGPLAYER == "mpg123") + mpegPlayer = DEFAULT_MPEGPLAYER ~ " -q "; + else if (DEFAULT_MPEGPLAYER == "mpv") + mpegPlayer = DEFAULT_MPEGPLAYER ~ " --msg-level=all=no "; + else + mpegPlayer = DEFAULT_MPEGPLAYER ~ " "; // spawn mpg123 - executeShell("mpg123 -q " ~ tmpf); + executeShell(mpegPlayer ~ tmpf); return 0; } } From dcb847578d7538dd4d53c79d3aa99fece0454b23 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Mon, 27 Mar 2023 12:52:36 +1300 Subject: [PATCH 004/133] Take non-lowercase options into account --- main.d | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/main.d b/main.d index 24c670b..20f5d2c 100644 --- a/main.d +++ b/main.d @@ -222,9 +222,13 @@ int main(string[] args) ~ "' is not convertible to an integer value; must be a non-decimal number"); } catch (IniException e) {} // just do nothing; use the default setting - if (optNoFootnotes) esv.opts.boolOpts["include_footnotes"] = false; - if (optNoHeadings) esv.opts.boolOpts["include_headings"] = false; - if (optNoVerseNumbers) esv.opts.boolOpts["include_verse_numbers"] = false; + if (optFootnotes) esv.opts.boolOpts["include_footnotes"] = true; + if (optHeadings) esv.opts.boolOpts["include_headings"] = true; + if (optVerseNumbers) esv.opts.boolOpts["include_verse_numbers"] = true; + if (optPassageReferences) esv.opts.boolOpts["include_passage_references"] = true; + if (optNoFootnotes) esv.opts.boolOpts["include_footnotes"] = false; + if (optNoHeadings) esv.opts.boolOpts["include_headings"] = false; + if (optNoVerseNumbers) esv.opts.boolOpts["include_verse_numbers"] = false; if (optNoPassageReferences) esv.opts.boolOpts["include_passage_references"] = false; if (optLineLength != 0) esv.opts.intOpts ["line_length"] = optLineLength; From 3751040fe04481797d4949bdeb141dd0a3dc06ef Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Mon, 27 Mar 2023 13:08:04 +1300 Subject: [PATCH 005/133] Use environment.get()'s second default value argument instead of using null checks --- main.d | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/main.d b/main.d index 20f5d2c..123db09 100644 --- a/main.d +++ b/main.d @@ -120,13 +120,12 @@ int main(string[] args) configPath = optConfigPath.expandTilde(); else panic(optConfigPath ~ ": invalid file path"); - } else if (configEnvVar !is null) { - if (configEnvVar.isValidPath()) - configPath = configEnvVar.expandTilde(); + } else { + configPath = environment.get(ENV_CONFIG, DEFAULT_CONFIGPATH); + if (configPath.isValidPath()) + configPath = configPath.expandTilde(); else panic(configEnvVar ~ ": invalid file path"); - } else { - configPath = DEFAULT_CONFIGPATH.expandTilde(); if (!configPath.exists()) { configPath.write( "# Default esv configuration file. From 0147fe1c60977cef639503a957201c09c05ec717 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Mon, 27 Mar 2023 13:32:18 +1300 Subject: [PATCH 006/133] Add support for changing the MP3 audio player through the ESV_PLAYER environment variable --- esv.1 | 14 ++++++++++++-- main.d | 12 +++++++----- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/esv.1 b/esv.1 index 0dea3c7..6ec92ab 100644 --- a/esv.1 +++ b/esv.1 @@ -18,11 +18,16 @@ is a program that displays passages of the Bible on your terminal. It can also play recorded audio tracks of certain passages, through integration with the .Xr mpg123 1 -utility. +utility. While audio is playing, you can use the standard mpg123 +controls: spacebar to pause/resume, comma to rewind, period +to fast-forward, etc. Read about the +.Fl C +option in mpg123's manual for more information. +.Pp If a text passage is too long for standard display on a terminal, .Nm will put it through a text pager (default less) in order for you to be able to -scroll through the text with ease. This behaviour can be disabled by passing +scroll through the text. This behaviour can be disabled by passing the .Fl P flag. @@ -73,6 +78,11 @@ What pager to use when the passage is over 32 lines long, rather than using the .Ic less utility. +.It Ev ESV_PLAYER +What MP3 player to use for playing audio, rather than using mpg123. +Using mpg123 is recommended over other players such as mpv, because +mpv's controls don't work well when started by another process +for some reason. .Sh FILES .Bl -tag -width ~/.config/esv.conf .It Pa ~/.config/esv.conf diff --git a/main.d b/main.d index 123db09..8e95f2e 100644 --- a/main.d +++ b/main.d @@ -38,6 +38,7 @@ enum DEFAULT_PAGER = "less"; enum ENV_CONFIG = "ESV_CONFIG"; enum ENV_PAGER = "ESV_PAGER"; +enum ENV_PLAYER = "ESV_PLAYER"; bool optAudio; string optConfigPath; @@ -175,14 +176,15 @@ int main(string[] args) return 1; } else { string tmpf = esv.getAudioVerses(args[1], args[2]); - string mpegPlayer; + string mpegPlayer = environment.get(ENV_PLAYER, DEFAULT_MPEGPLAYER); // esv has built-in support for mpg123 and mpv // other players will work, just recompile with // the DEFAULT_MPEGPLAYER enum set differently - if (DEFAULT_MPEGPLAYER == "mpg123") - mpegPlayer = DEFAULT_MPEGPLAYER ~ " -q "; - else if (DEFAULT_MPEGPLAYER == "mpv") - mpegPlayer = DEFAULT_MPEGPLAYER ~ " --msg-level=all=no "; + // or use the ESV_PLAYER environment variable + if (mpegPlayer == "mpg123") + mpegPlayer = mpegPlayer ~ " -q "; + else if (mpegPlayer == "mpv") + mpegPlayer = mpegPlayer ~ " --msg-level=all=no "; else mpegPlayer = DEFAULT_MPEGPLAYER ~ " "; // spawn mpg123 From fc932661132b9f2bd385f4dba3dcd124017c38e9 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Mon, 27 Mar 2023 13:58:05 +1300 Subject: [PATCH 007/133] Use a default approved API key and update to 0.2.0 --- main.d | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/main.d b/main.d index 8e95f2e..d8f537a 100644 --- a/main.d +++ b/main.d @@ -30,8 +30,9 @@ import std.string : splitLines; import esv; import dini; -enum VERSION = "0.1.0"; +enum VERSION = "0.2.0"; +enum DEFAULT_APIKEY = "abfb7456fa52ec4292c79e435890cfa3df14dc2b"; // crossway approved ;) enum DEFAULT_CONFIGPATH = "~/.config/esv.conf"; enum DEFAULT_MPEGPLAYER = "mpg123"; enum DEFAULT_PAGER = "less"; @@ -133,7 +134,7 @@ int main(string[] args) # An API key is required to access the ESV Bible API. [api] -#key = My API key here +key = " ~ DEFAULT_APIKEY ~ " # If you really need to, you can specify # custom API parameters using `parameters`: #parameters = &my-parameter=value From f2e056bf06c6e04c5d08b6b8fc5b472f7071c881 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Tue, 28 Mar 2023 10:44:58 +1300 Subject: [PATCH 008/133] License everything under the GPL version 2 --- COPYING | 35 ++--------------------------------- Makefile | 16 +++++++++++++++- README.md | 8 ++++---- esv.d | 37 +++++++++++++------------------------ main.d | 6 +++--- 5 files changed, 37 insertions(+), 65 deletions(-) diff --git a/COPYING b/COPYING index 0ed4056..f93c1b2 100644 --- a/COPYING +++ b/COPYING @@ -1,36 +1,5 @@ -All files except esv.d are licensed under the -GNU General Public License, version 3. - -The file esv.d is licensed under the -BSD 3-Clause License, which is as follows: - -The BSD 3-Clause License (BSD3) - -Copyright (c) 2023 Jeremy Baxter. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: -1. Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. -2. Redistributions in binary form must reproduce the above copyright -notice, this list of conditions and the following disclaimer in the -documentation and/or other materials provided with the distribution. -3. Neither the name of the copyright holder the nor the -names of its contributors may be used to endorse or promote products -derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER ''AS IS'' AND ANY -EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY -DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -The GNU General Public License is as follows: +This software is licensed under the GNU General Public License, version 2. +The license is as follows: GNU GENERAL PUBLIC LICENSE Version 2, June 1991 diff --git a/Makefile b/Makefile index 5be1678..0d9e3b9 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,18 @@ -### If you fail to build, run 'make deps'! ### +# The GPLv2 License (GPLv2) +# Copyright (c) 2023 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 . PROG = esv IMPORT = import diff --git a/README.md b/README.md index dafb069..c419413 100644 --- a/README.md +++ b/README.md @@ -66,12 +66,12 @@ All documentation is contained in the manual pages. To access them, you can run ## Copying -Copying, modifying and redistributing all files part of this software except esv.d is permitted -as long as your changed conform to the GNU General Public License version 2. +Copying, modifying and redistributing this software is permitted +as long as your modified version conforms to the GNU General Public License version 2. -The file esv.d is a reusable library. It is covered under the BSD 3-Clause License. +The file esv.d is a reusable library. -In both cases, the licenses are contained in the file COPYING. +The license is contained in the file COPYING. This software uses a modified version of a library named "dini". This is released under the Boost Software License version 1.0, which can be found in import/dini/LICENSE. diff --git a/esv.d b/esv.d index 13bdca1..4fc6a86 100644 --- a/esv.d +++ b/esv.d @@ -1,32 +1,21 @@ /* * esv.d: a reusable interface to the ESV HTTP API - * licensed under the BSD 3-Clause License: * - * The BSD 3-Clause License (BSD3) - * - * Copyright (c) 2023 Jeremy Baxter. All rights reserved. + * The GPLv2 License (GPLv2) + * Copyright (c) 2023 Jeremy Baxter * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * 3. Neither the name of the copyright holder the nor the - * names of its contributors may be used to endorse or promote products - * derived from this software without specific prior written permission. + * 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. * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER ''AS IS'' AND ANY - * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND - * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * 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 . */ import std.algorithm : filter, map; diff --git a/main.d b/main.d index d8f537a..9d6accb 100644 --- a/main.d +++ b/main.d @@ -4,18 +4,18 @@ * The GPLv2 License (GPLv2) * Copyright (c) 2023 Jeremy Baxter * - * This program is free software: you can redistribute it and/or modify + * 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. * - * This program is distributed in the hope that it will be useful, + * 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 this program. If not, see . + * along with esv. If not, see . */ import std.conv : to, ConvException; From dddf0db33fa15af6687cd93b02e0e084ea2ede24 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Thu, 11 May 2023 13:25:08 +1200 Subject: [PATCH 009/133] Add `in`, @nogc and @safe attributes to functions and parameters --- esv.d | 48 ++++++++++++++++++++---------------------------- 1 file changed, 20 insertions(+), 28 deletions(-) diff --git a/esv.d b/esv.d index 4fc6a86..a87e9d3 100644 --- a/esv.d +++ b/esv.d @@ -115,7 +115,7 @@ class EsvAPI string extraParameters; int delegate(size_t dlTotal, size_t dlNow, size_t ulTotal, size_t ulNow) onProgress; string tmpDir; - this(const string key) + this(in string key) { this._url = ESVAPI_URL; this._key = key; @@ -128,19 +128,19 @@ class EsvAPI /* * Returns the API URL currently in use. */ - final string getURL() const nothrow + final string getURL() const nothrow @nogc @safe { return _url; } /* * If the url argument is a valid HTTP URL, sets the API URL currently in use - * to the given url argument. Otherwise, throws a UrlException. + * to the given url argument. Otherwise, throws an EsvException . */ - final void setURL(const string url) + final void setURL(in string url) @safe { auto matches = url.matchAll("^https?://.+\\..+(/.+)?"); if (matches.empty) - throw new UrlException("Invalid URL format"); + throw new EsvException("Invalid URL format"); else this._url = url; } @@ -148,14 +148,14 @@ class EsvAPI * Returns the API authentication key that was given when the API object was instantiated. * This authentication key cannot be changed after instantiation. */ - final string getKey() const nothrow + final string getKey() const nothrow @nogc @safe { return _key; } /* * Returns the API authentication key currently in use. */ - final string getMode() const nothrow + final string getMode() const nothrow @nogc @safe { return _mode; } @@ -165,7 +165,7 @@ class EsvAPI * If the mode argument is not one of those, * then this function will do nothing. */ - final void setMode(const string mode) nothrow + final void setMode(in string mode) nothrow @nogc @safe { foreach (string m; ["text", "html"] ) { @@ -180,7 +180,7 @@ class EsvAPI * Returns true if the argument book is a valid book of the Bible. * Otherwise, returns false. */ - final bool validateBook(const string book) const nothrow + final bool validateBook(in string book) const nothrow @safe { foreach (string b; ESVAPI_BIBLE_BOOKS) { @@ -193,9 +193,9 @@ class EsvAPI * Returns true if the argument book is a valid verse format. * Otherwise, returns false. */ - final bool validateVerse(const string verse) const + final bool validateVerse(in string verse) const @safe { - bool attemptRegex(const string re) const + bool attemptRegex(string re) const @safe { auto matches = verse.matchAll(re); return !matches.empty; @@ -220,12 +220,12 @@ class EsvAPI * * Example: getVerses("John", "3:16-21") */ - final string getVerses(const string book, const string verse) const + final string getVerses(in string book, in string verse) const { if (!this.validateBook(book)) - throw new EsvPassageException("Invalid book"); + throw new EsvException("Invalid book"); if (!this.validateVerse(verse)) - throw new EsvPassageException("Invalid verse format"); + throw new EsvException("Invalid verse format"); string apiURL = format!"%s/%s/?q=%s+%s%s%s"(this._url, this._mode, book.capitalize().replaceAll(regex("_"), "+"), verse, this.assembleParameters(), this.extraParameters); @@ -250,12 +250,12 @@ class EsvAPI * * Example: getVerses("John", "3:16-21") */ - final string getAudioVerses(const string book, const string verse) + final string getAudioVerses(in string book, in string verse) const { if (!this.validateBook(book)) - throw new EsvPassageException("Invalid book"); + throw new EsvException("Invalid book"); if (!this.validateVerse(verse)) - throw new EsvPassageException("Invalid verse format"); + throw new EsvException("Invalid verse format"); string apiURL = format!"%s/audio/?q=%s+%s"(this._url, book.capitalize().replaceAll(regex("_"), "+"), verse); auto request = HTTP(apiURL); @@ -272,7 +272,7 @@ class EsvAPI tmpFile.write(response); return tmpFile; } - private string assembleParameters() const + private string assembleParameters() const @safe { string params = ""; string addParam(string param, string value) const @@ -317,7 +317,7 @@ struct EsvAPIOptions bool[string] boolOpts; int[string] intOpts; string indent_using; - void setDefaults() nothrow + void setDefaults() nothrow @safe { this.boolOpts["include_passage_references"] = true; this.boolOpts["include_verse_numbers"] = true; @@ -341,15 +341,7 @@ struct EsvAPIOptions } } -class UrlException : Exception -{ - this(string msg, string file = __FILE__, size_t line = __LINE__) @safe pure - { - super(msg, file, line); - } -} - -class EsvPassageException : Exception +class EsvException : Exception { this(string msg, string file = __FILE__, size_t line = __LINE__) @safe pure { From 6a6f39e0bd346a8df8c0889cf62ceaec0be467a4 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Thu, 11 May 2023 18:08:11 +1200 Subject: [PATCH 010/133] Remove gmake-exclusive ifeq statements in Makefile --- Makefile | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/Makefile b/Makefile index 0d9e3b9..8727760 100644 --- a/Makefile +++ b/Makefile @@ -1,19 +1,3 @@ -# The GPLv2 License (GPLv2) -# Copyright (c) 2023 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 . - PROG = esv IMPORT = import PREFIX = /usr/local @@ -23,16 +7,6 @@ DC = ldc2 CFLAGS = -O -I${IMPORT} OBJS = main.o esv.o ini.o -ifeq (${DEBUG},) - CFLAGS += -release -endif - -ifneq (${WI},) - CFLAGS += -wi -else - CFLAGS += -w -endif - all: esv esv: ${OBJS} From 56d69af099dd44ee62f3cb1e6e6db51cbb297b0e Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Thu, 11 May 2023 18:08:40 +1200 Subject: [PATCH 011/133] Add @safe attribute to main.extractOpt() --- main.d | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.d b/main.d index 9d6accb..1d73f26 100644 --- a/main.d +++ b/main.d @@ -265,7 +265,7 @@ key = " ~ DEFAULT_APIKEY ~ " return 0; } -string extractOpt(GetOptException e) +string extractOpt(GetOptException e) @safe { return e.msg.matchFirst("-.")[0]; } From 7d96227dc2796aee7db1115c15f5e7a42ee46df7 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Thu, 11 May 2023 18:10:05 +1200 Subject: [PATCH 012/133] Remove 'this' in front of every member variable in the EsvAPI class, and correct function parameter definitions --- esv.d | 136 +++++++++++++++++++++++++++++----------------------------- 1 file changed, 68 insertions(+), 68 deletions(-) diff --git a/esv.d b/esv.d index a87e9d3..c8c1286 100644 --- a/esv.d +++ b/esv.d @@ -115,15 +115,15 @@ class EsvAPI string extraParameters; int delegate(size_t dlTotal, size_t dlNow, size_t ulTotal, size_t ulNow) onProgress; string tmpDir; - this(in string key) + this(immutable(string) key) { - this._url = ESVAPI_URL; - this._key = key; - this._mode = "text"; - this.opts.setDefaults(); - this.extraParameters = ""; - this.onProgress = (size_t dlTotal, size_t dlNow, size_t ulTotal, size_t ulNow) {return 0;}; - this.tmpDir = tempDir() ~ "esvapi"; + _url = ESVAPI_URL; + _key = key; + _mode = "text"; + opts.setDefaults(); + extraParameters = ""; + onProgress = (size_t dlTotal, size_t dlNow, size_t ulTotal, size_t ulNow) {return 0;}; + tmpDir = tempDir() ~ "esvapi"; } /* * Returns the API URL currently in use. @@ -136,13 +136,13 @@ class EsvAPI * If the url argument is a valid HTTP URL, sets the API URL currently in use * to the given url argument. Otherwise, throws an EsvException . */ - final void setURL(in string url) @safe + final void setURL(immutable(string) url) @safe { auto matches = url.matchAll("^https?://.+\\..+(/.+)?"); if (matches.empty) throw new EsvException("Invalid URL format"); else - this._url = url; + _url = url; } /* * Returns the API authentication key that was given when the API object was instantiated. @@ -165,13 +165,13 @@ class EsvAPI * If the mode argument is not one of those, * then this function will do nothing. */ - final void setMode(in string mode) nothrow @nogc @safe + final void setMode(immutable(string) mode) nothrow @nogc @safe { foreach (string m; ["text", "html"] ) { if (mode == m) { - this._mode = mode; + _mode = mode; return; } } @@ -180,7 +180,7 @@ class EsvAPI * Returns true if the argument book is a valid book of the Bible. * Otherwise, returns false. */ - final bool validateBook(in string book) const nothrow @safe + final bool validateBook(in char[] book) const nothrow @safe { foreach (string b; ESVAPI_BIBLE_BOOKS) { @@ -193,7 +193,7 @@ class EsvAPI * Returns true if the argument book is a valid verse format. * Otherwise, returns false. */ - final bool validateVerse(in string verse) const @safe + final bool validateVerse(in char[] verse) const @safe { bool attemptRegex(string re) const @safe { @@ -220,24 +220,24 @@ class EsvAPI * * Example: getVerses("John", "3:16-21") */ - final string getVerses(in string book, in string verse) const + final string getVerses(in char[] book, in char[] verse) const { - if (!this.validateBook(book)) + if (!validateBook(book)) throw new EsvException("Invalid book"); - if (!this.validateVerse(verse)) + if (!validateVerse(verse)) throw new EsvException("Invalid verse format"); - string apiURL = format!"%s/%s/?q=%s+%s%s%s"(this._url, this._mode, - book.capitalize().replaceAll(regex("_"), "+"), verse, this.assembleParameters(), this.extraParameters); + string apiURL = format!"%s/%s/?q=%s+%s%s%s"(_url, _mode, + book.capitalize().replaceAll(regex("_"), "+"), verse, assembleParameters(), extraParameters); auto request = HTTP(apiURL); string response; - request.onProgress = this.onProgress; + request.onProgress = onProgress; request.onReceive = (ubyte[] data) { response = cast(string)data; return data.length; }; - request.addRequestHeader("Authorization", "Token " ~ this._key); + request.addRequestHeader("Authorization", "Token " ~ _key); request.perform(); return response.parseJSON()["passages"][0].str; } @@ -250,23 +250,23 @@ class EsvAPI * * Example: getVerses("John", "3:16-21") */ - final string getAudioVerses(in string book, in string verse) const + final string getAudioVerses(in char[] book, in char[] verse) const { - if (!this.validateBook(book)) + if (!validateBook(book)) throw new EsvException("Invalid book"); - if (!this.validateVerse(verse)) + if (!validateVerse(verse)) throw new EsvException("Invalid verse format"); - string apiURL = format!"%s/audio/?q=%s+%s"(this._url, book.capitalize().replaceAll(regex("_"), "+"), verse); + string apiURL = format!"%s/audio/?q=%s+%s"(_url, book.capitalize().replaceAll(regex("_"), "+"), verse); auto request = HTTP(apiURL); ubyte[] response; - request.onProgress = this.onProgress; + request.onProgress = onProgress; request.onReceive = (ubyte[] data) { response = response ~= data; return data.length; }; - request.addRequestHeader("Authorization", "Token " ~ this._key); + request.addRequestHeader("Authorization", "Token " ~ _key); request.perform(); string tmpFile = tempFile(); tmpFile.write(response); @@ -279,34 +279,34 @@ class EsvAPI { return format!"%s&%s=%s"(params, param, value); } - params = addParam("include-passage-references", this.opts.boolOpts["include_passage_references"].to!string); - params = addParam("include-verse-numbers", this.opts.boolOpts["include_verse_numbers"].to!string); - params = addParam("include-first-verse-numbers", this.opts.boolOpts["include_first_verse_numbers"].to!string); - params = addParam("include-footnotes", this.opts.boolOpts["include_footnotes"].to!string); - params = addParam("include-footnote-body", this.opts.boolOpts["include_footnote_body"].to!string); - params = addParam("include-headings", this.opts.boolOpts["include_headings"].to!string); - params = addParam("include-short-copyright", this.opts.boolOpts["include_short_copyright"].to!string); - params = addParam("include-copyright", this.opts.boolOpts["include_copyright"].to!string); - params = addParam("include-passage-horizontal-lines", this.opts.boolOpts["include_passage_horizontal_lines"].to!string); - params = addParam("include-heading-horizontal-lines", this.opts.boolOpts["include_heading_horizontal_lines"].to!string); - params = addParam("include-selahs", this.opts.boolOpts["include_selahs"].to!string); - params = addParam("indent-poetry", this.opts.boolOpts["indent_poetry"].to!string); - params = addParam("horizontal-line-length", this.opts.intOpts ["horizontal_line_length"].to!string); - params = addParam("indent-paragraphs", this.opts.intOpts ["indent_paragraphs"].to!string); - params = addParam("indent-poetry-lines", this.opts.intOpts ["indent_poetry_lines"].to!string); - params = addParam("indent-declares", this.opts.intOpts ["indent_declares"].to!string); - params = addParam("indent-psalm-doxology", this.opts.intOpts ["indent_psalm_doxology"].to!string); - params = addParam("line-length", this.opts.intOpts ["line_length"].to!string); - params = addParam("indent-using", this.opts.indent_using.to!string); + params = addParam("include-passage-references", opts.boolOpts["include_passage_references"].to!string); + params = addParam("include-verse-numbers", opts.boolOpts["include_verse_numbers"].to!string); + params = addParam("include-first-verse-numbers", opts.boolOpts["include_first_verse_numbers"].to!string); + params = addParam("include-footnotes", opts.boolOpts["include_footnotes"].to!string); + params = addParam("include-footnote-body", opts.boolOpts["include_footnote_body"].to!string); + params = addParam("include-headings", opts.boolOpts["include_headings"].to!string); + params = addParam("include-short-copyright", opts.boolOpts["include_short_copyright"].to!string); + params = addParam("include-copyright", opts.boolOpts["include_copyright"].to!string); + params = addParam("include-passage-horizontal-lines", opts.boolOpts["include_passage_horizontal_lines"].to!string); + params = addParam("include-heading-horizontal-lines", opts.boolOpts["include_heading_horizontal_lines"].to!string); + params = addParam("include-selahs", opts.boolOpts["include_selahs"].to!string); + params = addParam("indent-poetry", opts.boolOpts["indent_poetry"].to!string); + params = addParam("horizontal-line-length", opts.intOpts ["horizontal_line_length"].to!string); + params = addParam("indent-paragraphs", opts.intOpts ["indent_paragraphs"].to!string); + params = addParam("indent-poetry-lines", opts.intOpts ["indent_poetry_lines"].to!string); + params = addParam("indent-declares", opts.intOpts ["indent_declares"].to!string); + params = addParam("indent-psalm-doxology", opts.intOpts ["indent_psalm_doxology"].to!string); + params = addParam("line-length", opts.intOpts ["line_length"].to!string); + params = addParam("indent-using", opts.indent_using.to!string); return params; } - private string tempFile() const + private string tempFile() const @safe { auto rndNums = rndGen().map!(a => cast(ubyte)a)().take(32); auto result = appender!string(); Base64.encode(rndNums, result); - this.tmpDir.mkdirRecurse(); - string f = this.tmpDir ~ "/" ~ result.data.filter!isAlphaNum().to!string(); + tmpDir.mkdirRecurse(); + string f = tmpDir ~ "/" ~ result.data.filter!isAlphaNum().to!string(); f.write(""); return f; } @@ -319,25 +319,25 @@ struct EsvAPIOptions string indent_using; void setDefaults() nothrow @safe { - this.boolOpts["include_passage_references"] = true; - this.boolOpts["include_verse_numbers"] = true; - this.boolOpts["include_first_verse_numbers"] = true; - this.boolOpts["include_footnotes"] = true; - this.boolOpts["include_footnote_body"] = true; - this.boolOpts["include_headings"] = true; - this.boolOpts["include_short_copyright"] = true; - this.boolOpts["include_copyright"] = false; - this.boolOpts["include_passage_horizontal_lines"] = false; - this.boolOpts["include_heading_horizontal_lines"] = false; - this.boolOpts["include_selahs"] = true; - this.boolOpts["indent_poetry"] = true; - this.intOpts["horizontal_line_length"] = 55; - this.intOpts["indent_paragraphs"] = 2; - this.intOpts["indent_poetry_lines"] = 4; - this.intOpts["indent_declares"] = 40; - this.intOpts["indent_psalm_doxology"] = 30; - this.intOpts["line_length"] = 0; - this.indent_using = "space"; + boolOpts["include_passage_references"] = true; + boolOpts["include_verse_numbers"] = true; + boolOpts["include_first_verse_numbers"] = true; + boolOpts["include_footnotes"] = true; + boolOpts["include_footnote_body"] = true; + boolOpts["include_headings"] = true; + boolOpts["include_short_copyright"] = true; + boolOpts["include_copyright"] = false; + boolOpts["include_passage_horizontal_lines"] = false; + boolOpts["include_heading_horizontal_lines"] = false; + boolOpts["include_selahs"] = true; + boolOpts["indent_poetry"] = true; + intOpts["horizontal_line_length"] = 55; + intOpts["indent_paragraphs"] = 2; + intOpts["indent_poetry_lines"] = 4; + intOpts["indent_declares"] = 40; + intOpts["indent_psalm_doxology"] = 30; + intOpts["line_length"] = 0; + indent_using = "space"; } } From 64fca717c611841a1b38db47559855ece716a452 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Thu, 11 May 2023 18:58:16 +1200 Subject: [PATCH 013/133] Reduce repetition in Makefile --- Makefile | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index 8727760..b03e8bf 100644 --- a/Makefile +++ b/Makefile @@ -1,26 +1,25 @@ -PROG = esv IMPORT = import PREFIX = /usr/local MANPREFIX = /usr/share/man DC = ldc2 -CFLAGS = -O -I${IMPORT} +CFLAGS = -O -I${IMPORT} -w OBJS = main.o esv.o ini.o all: esv esv: ${OBJS} - ${DC} ${CFLAGS} -of=${PROG} ${OBJS} + ${DC} ${CFLAGS} -of$@ ${OBJS} # main executable main.o: main.d esv.o - ${DC} -c ${CFLAGS} main.d -of=main.o + ${DC} ${CFLAGS} -c main.d -of$@ esv.o: esv.d - ${DC} -c -i ${CFLAGS} esv.d -of=esv.o + ${DC} ${CFLAGS} -c esv.d -of$@ ini.o: ${IMPORT}/dini/*.d - ${DC} -c -i ${CFLAGS} ${IMPORT}/dini/*.d -of=ini.o + ${DC} ${CFLAGS} -c ${IMPORT}/dini/*.d -of$@ clean: rm -f ${PROG} ${OBJS} From 7f61d7bfa6afef7bfee7c891acd1f4afcf8dc67e Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Thu, 11 May 2023 18:59:13 +1200 Subject: [PATCH 014/133] Rename ESVAPI_BIBLE_BOOKS to BIBLE_BOOKS and make it use spaces for book names, instead of underscores. --- esv.d | 44 ++++++++++++++++++++++---------------------- main.d | 25 +++++++++++++++---------- 2 files changed, 37 insertions(+), 32 deletions(-) diff --git a/esv.d b/esv.d index c8c1286..1093b07 100644 --- a/esv.d +++ b/esv.d @@ -34,7 +34,7 @@ import std.utf : toUTF8; import std.net.curl; const enum ESVAPI_URL = "https://api.esv.org/v3/passage"; -const string[] ESVAPI_BIBLE_BOOKS = [ +const string[] BIBLE_BOOKS = [ // Old Testament "Genesis", "Exodus", @@ -44,12 +44,12 @@ const string[] ESVAPI_BIBLE_BOOKS = [ "Joshua", "Judges", "Ruth", - "1_Samuel", - "2_Samuel", - "1_Kings", - "2_Kings", - "1_Chronicles", - "2_Chronicles", + "1 Samuel", + "2 Samuel", + "1 Kings", + "2 Kings", + "1 Chronicles", + "2 Chronicles", "Ezra", "Nehemiah", "Esther", @@ -58,7 +58,7 @@ const string[] ESVAPI_BIBLE_BOOKS = [ "Psalms", // <- both are valid "Proverbs", "Ecclesiastes", - "Song_of_Solomon", + "Song of Solomon", "Isaiah", "Jeremiah", "Lamentations", @@ -83,25 +83,25 @@ const string[] ESVAPI_BIBLE_BOOKS = [ "John", "Acts", "Romans", - "1_Corinthians", - "2_Corinthians", + "1 Corinthians", + "2 Corinthians", "Galatians", "Ephesians", "Philippians", "Colossians", - "1_Thessalonians", - "2_Thessalonians", - "1_Timothy", - "2_Timothy", + "1 Thessalonians", + "2 Thessalonians", + "1 Timothy", + "2 Timothy", "Titus", "Philemon", "Hebrews", "James", - "1_Peter", - "2_Peter", - "1_John", - "2_John", - "3_John", + "1 Peter", + "2 Peter", + "1 John", + "2 John", + "3 John", "Jude", "Revelation" ]; @@ -182,7 +182,7 @@ class EsvAPI */ final bool validateBook(in char[] book) const nothrow @safe { - foreach (string b; ESVAPI_BIBLE_BOOKS) + foreach (string b; BIBLE_BOOKS) { if (book.capitalize() == b.capitalize()) return true; @@ -228,7 +228,7 @@ class EsvAPI throw new EsvException("Invalid verse format"); string apiURL = format!"%s/%s/?q=%s+%s%s%s"(_url, _mode, - book.capitalize().replaceAll(regex("_"), "+"), verse, assembleParameters(), extraParameters); + book.capitalize().replaceAll(regex(" "), "+"), verse, assembleParameters(), extraParameters); auto request = HTTP(apiURL); string response; request.onProgress = onProgress; @@ -257,7 +257,7 @@ class EsvAPI if (!validateVerse(verse)) throw new EsvException("Invalid verse format"); - string apiURL = format!"%s/audio/?q=%s+%s"(_url, book.capitalize().replaceAll(regex("_"), "+"), verse); + string apiURL = format!"%s/audio/?q=%s+%s"(_url, book.capitalize().replaceAll(regex(" "), "+"), verse); auto request = HTTP(apiURL); ubyte[] response; request.onProgress = onProgress; diff --git a/main.d b/main.d index 1d73f26..4d3c6bf 100644 --- a/main.d +++ b/main.d @@ -23,7 +23,7 @@ import std.file : exists, write, FileException; import std.getopt : getopt, GetOptException, config; import std.path : baseName, expandTilde, isValidPath; import std.process : environment, executeShell; -import std.regex : regex, matchFirst, replaceFirst; +import std.regex : regex, matchFirst, replaceAll, replaceFirst; import std.stdio : writef, writeln, writefln, stderr; import std.string : splitLines; @@ -165,7 +165,7 @@ key = " ~ DEFAULT_APIKEY ~ " // Initialise API object and validate the book and verse EsvAPI esv = new EsvAPI(apiKey); - if (!esv.validateBook(args[1])) + if (!esv.validateBook(args[1].extractBook())) panic("book '" ~ args[1] ~ "' does not exist"); if (!esv.validateVerse(args[2])) panic("invalid verse format '" ~ args[2] ~ "'"); @@ -209,9 +209,9 @@ key = " ~ DEFAULT_APIKEY ~ " try { esv.opts.boolOpts["include_" ~ key] = returnValid("true", iniData["passage"].getKey(key)).catchConvException( - (ConvException ex, string str) + (in ConvException ex, in char[] str) { - panic(configPath ~ ": value '" ~ str ~ + panic(configPath ~ ": value '" ~ cast(string)str ~ "' is not convertible to a boolean value; must be either 'true' or 'false'"); } ); @@ -234,7 +234,7 @@ key = " ~ DEFAULT_APIKEY ~ " if (optNoPassageReferences) esv.opts.boolOpts["include_passage_references"] = false; if (optLineLength != 0) esv.opts.intOpts ["line_length"] = optLineLength; - string verses = esv.getVerses(args[1], args[2]); + string verses = esv.getVerses(args[1].extractBook(), args[2]); int lines; foreach (string line; verses.splitLines()) ++lines; @@ -253,9 +253,9 @@ key = " ~ DEFAULT_APIKEY ~ " catch (ProcessException e) { if (!e.msg.matchFirst(regex("^Executable file not found")).empty) { panic(e.msg - .matchFirst(": (.+)$")[0] - .replaceFirst(regex("^: "), "") - ~ ": command not found" + .matchFirst(": (.+)$")[0] + .replaceFirst(regex("^: "), "") + ~ ": command not found" ); } } @@ -265,12 +265,17 @@ key = " ~ DEFAULT_APIKEY ~ " return 0; } -string extractOpt(GetOptException e) @safe +string extractOpt(in GetOptException e) @safe { return e.msg.matchFirst("-.")[0]; } -bool catchConvException(string sb, void delegate(ConvException ex, string str) catchNet) +string extractBook(in string book) @safe +{ + return book.replaceAll(regex("[-_]"), " "); +} + +bool catchConvException(in char[] sb, void delegate(in ConvException ex, in char[] str) @system catchNet) { try return sb.to!bool(); catch (ConvException e) { From 5dc2a12f1ec8c23b89a6143ffbcbc6a2990b09ad Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Sat, 3 Jun 2023 18:52:24 +1200 Subject: [PATCH 015/133] Refactor config parsing code in main.d, rename esv.d -> esvapi.d, rename class EsvAPI to ESVApi, makefile changes --- Makefile | 14 +++++++------- esv.d => esvapi.d | 10 ++++++---- main.d | 30 +++++++++++------------------- 3 files changed, 24 insertions(+), 30 deletions(-) rename esv.d => esvapi.d (98%) diff --git a/Makefile b/Makefile index b03e8bf..0593c8c 100644 --- a/Makefile +++ b/Makefile @@ -2,9 +2,9 @@ IMPORT = import PREFIX = /usr/local MANPREFIX = /usr/share/man -DC = ldc2 -CFLAGS = -O -I${IMPORT} -w -OBJS = main.o esv.o ini.o +DC = ldc2 +CFLAGS = -O -I${IMPORT} -release -w +OBJS = main.o esvapi.o ini.o all: esv @@ -12,17 +12,17 @@ esv: ${OBJS} ${DC} ${CFLAGS} -of$@ ${OBJS} # main executable -main.o: main.d esv.o +main.o: main.d esvapi.o ${DC} ${CFLAGS} -c main.d -of$@ -esv.o: esv.d - ${DC} ${CFLAGS} -c esv.d -of$@ +esvapi.o: esvapi.d + ${DC} ${CFLAGS} -c esvapi.d -of$@ ini.o: ${IMPORT}/dini/*.d ${DC} ${CFLAGS} -c ${IMPORT}/dini/*.d -of$@ clean: - rm -f ${PROG} ${OBJS} + rm -f esv ${OBJS} install: esv install -m755 esv ${DESTDIR}${PREFIX}/bin/esv diff --git a/esv.d b/esvapi.d similarity index 98% rename from esv.d rename to esvapi.d index 1093b07..f47c7de 100644 --- a/esv.d +++ b/esvapi.d @@ -18,6 +18,8 @@ * along with esv. If not, see . */ +module esvapi; + import std.algorithm : filter, map; import std.array : appender; import std.ascii : isAlphaNum; @@ -106,12 +108,12 @@ const string[] BIBLE_BOOKS = [ "Revelation" ]; -class EsvAPI +class ESVApi { private string _key; private string _url; private string _mode; - EsvAPIOptions opts; + ESVApiOptions opts; string extraParameters; int delegate(size_t dlTotal, size_t dlNow, size_t ulTotal, size_t ulNow) onProgress; string tmpDir; @@ -312,7 +314,7 @@ class EsvAPI } } -struct EsvAPIOptions +struct ESVApiOptions { bool[string] boolOpts; int[string] intOpts; @@ -337,7 +339,7 @@ struct EsvAPIOptions intOpts["indent_declares"] = 40; intOpts["indent_psalm_doxology"] = 30; intOpts["line_length"] = 0; - indent_using = "space"; + indent_using = "space"; } } diff --git a/main.d b/main.d index 4d3c6bf..624f8bd 100644 --- a/main.d +++ b/main.d @@ -20,6 +20,7 @@ import std.conv : to, ConvException; import std.file : exists, write, FileException; +import std.format : format; import std.getopt : getopt, GetOptException, config; import std.path : baseName, expandTilde, isValidPath; import std.process : environment, executeShell; @@ -27,7 +28,7 @@ import std.regex : regex, matchFirst, replaceAll, replaceFirst; import std.stdio : writef, writeln, writefln, stderr; import std.string : splitLines; -import esv; +import esvapi; import dini; enum VERSION = "0.2.0"; @@ -164,7 +165,7 @@ key = " ~ DEFAULT_APIKEY ~ " panic("API key not present in configuration file; cannot proceed"); // Initialise API object and validate the book and verse - EsvAPI esv = new EsvAPI(apiKey); + ESVApi esv = new ESVApi(apiKey); if (!esv.validateBook(args[1].extractBook())) panic("book '" ~ args[1] ~ "' does not exist"); if (!esv.validateVerse(args[2])) @@ -208,17 +209,17 @@ key = " ~ DEFAULT_APIKEY ~ " foreach (string key; ["footnotes", "headings", "passage_references", "verse_numbers"]) { try { esv.opts.boolOpts["include_" ~ key] = - returnValid("true", iniData["passage"].getKey(key)).catchConvException( - (in ConvException ex, in char[] str) - { - panic(configPath ~ ": value '" ~ cast(string)str ~ - "' is not convertible to a boolean value; must be either 'true' or 'false'"); - } - ); + returnValid("true", iniData["passage"].getKey(key)).to!bool(); + } catch (ConvException e) { + panic( + format!"%s: key '%s' of section 'passage' is not a boolean value (must be either 'true' or 'false')"( + configPath, key + ) + ); } catch (IniException e) {} // just do nothing; use the default settings } // Get line_length ([passage]) - try esv.opts.intOpts["line_length"] = returnValid("0", iniData["passage"].getKey("line_length")).to!int(); + try esv.opts.intOpts["line_length"] = returnValid("0", iniData["passage"].getKey("line_length")).to!int(); catch (ConvException e) { panic(configPath ~ ": value '" ~ iniData["passage"].getKey("line_length") ~ "' is not convertible to an integer value; must be a non-decimal number"); @@ -274,12 +275,3 @@ string extractBook(in string book) @safe { return book.replaceAll(regex("[-_]"), " "); } - -bool catchConvException(in char[] sb, void delegate(in ConvException ex, in char[] str) @system catchNet) -{ - try return sb.to!bool(); - catch (ConvException e) { - catchNet(e, sb); - return false; - } -} From 0285e0979de35947bfab5f78248888ac6909040f Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Sat, 3 Jun 2023 19:00:13 +1200 Subject: [PATCH 016/133] More makefile changes --- Makefile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 0593c8c..6bd438e 100644 --- a/Makefile +++ b/Makefile @@ -13,13 +13,13 @@ esv: ${OBJS} # main executable main.o: main.d esvapi.o - ${DC} ${CFLAGS} -c main.d -of$@ + ${DC} ${CFLAGS} -of$@ -c main.d esvapi.o: esvapi.d - ${DC} ${CFLAGS} -c esvapi.d -of$@ + ${DC} ${CFLAGS} -of$@ -c esvapi.d ini.o: ${IMPORT}/dini/*.d - ${DC} ${CFLAGS} -c ${IMPORT}/dini/*.d -of$@ + ${DC} ${CFLAGS} -of$@ -c ${IMPORT}/dini/*.d clean: rm -f esv ${OBJS} @@ -29,4 +29,4 @@ install: esv cp -f esv.1 ${DESTDIR}${MANPREFIX}/man1 cp -f esv.conf.5 ${DESTDIR}${MANPREFIX}/man5 -.PHONY: clean install +.PHONY: all clean install From d3558feb350329a506de3453e03a3a75960b93a7 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Sat, 3 Jun 2023 19:12:19 +1200 Subject: [PATCH 017/133] Readme changes --- README.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index c419413..5bfc305 100644 --- a/README.md +++ b/README.md @@ -38,8 +38,10 @@ an audio passage of Matthew 5-7. ## Installation -To install `esv`, first make sure you have the [LLVM D compiler (ldc)](https://github.com/ldc-developers/ldc#installation) installed on your system. -There are no other external dependencies. +To install `esv`, first make sure you have the +[LLVM D compiler (ldc)](https://github.com/ldc-developers/ldc#installation) +installed on your system. You should also have Phobos (the D standard library, comes included with LDC) +installed as a dynamic library in order to run the executable. First clone the source code repository: @@ -56,8 +58,8 @@ $ make ``` By default the Makefile guesses that the ldc executable is named `ldc2`. If it is installed -under `ldc` instead, you can override the default D compiler executable by adding `DC=ldc` -to the end of the `make` line. +under a different name, or if you wish to use a different compiler, use `make DC=compiler` +(where `compiler` is your compiler) instead. ## Documentation @@ -69,7 +71,7 @@ All documentation is contained in the manual pages. To access them, you can run Copying, modifying and redistributing this software is permitted as long as your modified version conforms to the GNU General Public License version 2. -The file esv.d is a reusable library. +The file esvapi.d is a reusable library; all documentation is provided in the source file. The license is contained in the file COPYING. From 45890a60512b62230a8aa53c84a9e38da0db9d50 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Sat, 3 Jun 2023 19:40:30 +1200 Subject: [PATCH 018/133] esv.d -> esvapi.d --- esvapi.d | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esvapi.d b/esvapi.d index f47c7de..eb41bc4 100644 --- a/esvapi.d +++ b/esvapi.d @@ -1,5 +1,5 @@ /* - * esv.d: a reusable interface to the ESV HTTP API + * esvapi.d: a reusable interface to the ESV HTTP API * * The GPLv2 License (GPLv2) * Copyright (c) 2023 Jeremy Baxter From 8564fd30030c094e15386540dff71f44306ff489 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Sun, 4 Jun 2023 21:13:37 +1200 Subject: [PATCH 019/133] Heavily refactor esvapi.d (add ESVMode enum, ...) and apply changes to main.d --- esvapi.d | 185 +++++++++++++++++++++++++++++++------------------------ main.d | 22 ++++--- 2 files changed, 117 insertions(+), 90 deletions(-) diff --git a/esvapi.d b/esvapi.d index eb41bc4..ca8ae44 100644 --- a/esvapi.d +++ b/esvapi.d @@ -30,11 +30,18 @@ import std.format : format; import std.json : JSONValue, parseJSON; import std.random : rndGen; import std.range : take; -import std.regex : matchAll, replaceAll, regex; +import std.regex : matchAll, replaceAll, replaceFirst, regex; import std.string : capitalize; import std.utf : toUTF8; import std.net.curl; +public enum ESVMode +{ + TEXT, + AUDIO +} + +const enum ESVAPI_KEY = "abfb7456fa52ec4292c79e435890cfa3df14dc2b"; const enum ESVAPI_URL = "https://api.esv.org/v3/passage"; const string[] BIBLE_BOOKS = [ // Old Testament @@ -56,8 +63,8 @@ const string[] BIBLE_BOOKS = [ "Nehemiah", "Esther", "Job", - "Psalm", // <- - "Psalms", // <- both are valid + "Psalm", + "Psalms", // both are valid "Proverbs", "Ecclesiastes", "Song of Solomon", @@ -110,54 +117,38 @@ const string[] BIBLE_BOOKS = [ class ESVApi { - private string _key; - private string _url; - private string _mode; + private { + int _mode; + string _key; + string _tmp; + string _url; + } ESVApiOptions opts; string extraParameters; - int delegate(size_t dlTotal, size_t dlNow, size_t ulTotal, size_t ulNow) onProgress; - string tmpDir; - this(immutable(string) key) + int delegate(size_t, size_t, size_t, size_t) onProgress; + this(immutable(string) key = ESVAPI_KEY, bool audio = false) { - _url = ESVAPI_URL; _key = key; - _mode = "text"; - opts.setDefaults(); + _mode = audio ? ESVMode.AUDIO : ESVMode.TEXT; + _tmp = tempDir() ~ "esv"; + _url = ESVAPI_URL; + opts.defaults(); extraParameters = ""; - onProgress = (size_t dlTotal, size_t dlNow, size_t ulTotal, size_t ulNow) {return 0;}; - tmpDir = tempDir() ~ "esvapi"; - } - /* - * Returns the API URL currently in use. - */ - final string getURL() const nothrow @nogc @safe - { - return _url; - } - /* - * If the url argument is a valid HTTP URL, sets the API URL currently in use - * to the given url argument. Otherwise, throws an EsvException . - */ - final void setURL(immutable(string) url) @safe - { - auto matches = url.matchAll("^https?://.+\\..+(/.+)?"); - if (matches.empty) - throw new EsvException("Invalid URL format"); - else - _url = url; + onProgress = (size_t dlTotal, size_t dlNow, size_t ulTotal, size_t ulNow) { return 0; }; + tmpName = "esv"; } /* * Returns the API authentication key that was given when the API object was instantiated. * This authentication key cannot be changed after instantiation. */ - final string getKey() const nothrow @nogc @safe + @nogc @property @safe string key() const nothrow { return _key; } /* * Returns the API authentication key currently in use. */ - final string getMode() const nothrow @nogc @safe + @nogc @property @safe int mode() const nothrow { return _mode; } @@ -165,27 +156,54 @@ class ESVApi * If the mode argument is either "text" or "html", * sets the text API mode to the given mode argument. * If the mode argument is not one of those, - * then this function will do nothing. + * throws an ESVException. */ - final void setMode(immutable(string) mode) nothrow @nogc @safe + @property @safe void mode(immutable(int) mode) { - foreach (string m; ["text", "html"] ) - { - if (mode == m) - { - _mode = mode; - return; - } - } + if (mode == ESVMode.TEXT || mode == ESVMode.AUDIO) + _mode = mode; + else + throw new ESVException("Invalid mode"); + } + /* + * Returns the API URL currently in use. + */ + @nogc @property @safe string url() const nothrow + { + return _url; + } + /* + * If the url argument is a valid HTTP URL, sets the API URL currently in use + * to the given url argument. Otherwise, throws an ESVException. + */ + @property @safe void url(immutable(string) url) + { + if (url.matchAll("^https?://.+\\..+(/.+)?").empty) + throw new ESVException("Invalid URL format"); + else + _url = url; + } + /* + * Returns the temp directory name. + */ + @property @safe tmpName() const + { + return _tmp.replaceFirst(regex('^' ~ tempDir()), ""); + } + /* + * Sets the temp directory name to the given string. + */ + @property @safe void tmpName(immutable(string) name) + { + _tmp = tempDir() ~ name; } /* * Returns true if the argument book is a valid book of the Bible. * Otherwise, returns false. */ - final bool validateBook(in char[] book) const nothrow @safe + @safe bool validateBook(in char[] book) const nothrow { - foreach (string b; BIBLE_BOOKS) - { + foreach (b; BIBLE_BOOKS) { if (book.capitalize() == b.capitalize()) return true; } @@ -195,42 +213,47 @@ class ESVApi * Returns true if the argument book is a valid verse format. * Otherwise, returns false. */ - final bool validateVerse(in char[] verse) const @safe + @safe bool validateVerse(in char[] verse) const { - bool attemptRegex(string re) const @safe + @safe bool attemptRegex(string re) const { - auto matches = verse.matchAll(re); - return !matches.empty; + return !verse.matchAll(re).empty; } if (attemptRegex("^\\d{1,3}$") || - attemptRegex("^\\d{1,3}-\\d{1,3}$") || - attemptRegex("^\\d{1,3}:\\d{1,3}$") || - attemptRegex("^\\d{1,3}:\\d{1,3}-\\d{1,3}$")) - { + attemptRegex("^\\d{1,3}-\\d{1,3}$") || + attemptRegex("^\\d{1,3}:\\d{1,3}$") || + attemptRegex("^\\d{1,3}:\\d{1,3}-\\d{1,3}$")) return true; - } else - { return false; - } } /* * Requests the verse(s) from the API and returns it. * The (case-insensitive) name of the book being searched are * contained in the argument book. The verse(s) being looked up are * contained in the argument verses. + * + * If the mode is ESVMode.AUDIO, requests an audio passage instead. + * A file path to an MP3 audio track is returned. + * To explicitly get an audio passage without setting the mode, + * use getAudioVerses(). * * Example: getVerses("John", "3:16-21") */ - final string getVerses(in char[] book, in char[] verse) const + string getVerses(in char[] book, in char[] verse) const { + if (_mode == ESVMode.AUDIO) { + return getAudioVerses(book, verse); + } + if (!validateBook(book)) - throw new EsvException("Invalid book"); + throw new ESVException("Invalid book"); if (!validateVerse(verse)) - throw new EsvException("Invalid verse format"); + throw new ESVException("Invalid verse format"); string apiURL = format!"%s/%s/?q=%s+%s%s%s"(_url, _mode, - book.capitalize().replaceAll(regex(" "), "+"), verse, assembleParameters(), extraParameters); + book.capitalize().replaceAll(regex(" "), "+"), verse, + assembleParameters(), extraParameters); auto request = HTTP(apiURL); string response; request.onProgress = onProgress; @@ -252,12 +275,12 @@ class ESVApi * * Example: getVerses("John", "3:16-21") */ - final string getAudioVerses(in char[] book, in char[] verse) const + string getAudioVerses(in char[] book, in char[] verse) const { if (!validateBook(book)) - throw new EsvException("Invalid book"); + throw new ESVException("Invalid book"); if (!validateVerse(verse)) - throw new EsvException("Invalid verse format"); + throw new ESVException("Invalid verse format"); string apiURL = format!"%s/audio/?q=%s+%s"(_url, book.capitalize().replaceAll(regex(" "), "+"), verse); auto request = HTTP(apiURL); @@ -274,7 +297,8 @@ class ESVApi tmpFile.write(response); return tmpFile; } - private string assembleParameters() const @safe + private: + @safe string assembleParameters() const { string params = ""; string addParam(string param, string value) const @@ -302,14 +326,13 @@ class ESVApi params = addParam("indent-using", opts.indent_using.to!string); return params; } - private string tempFile() const @safe + @safe string tempFile() const { auto rndNums = rndGen().map!(a => cast(ubyte)a)().take(32); auto result = appender!string(); Base64.encode(rndNums, result); - tmpDir.mkdirRecurse(); - string f = tmpDir ~ "/" ~ result.data.filter!isAlphaNum().to!string(); - f.write(""); + _tmp.mkdirRecurse(); + string f = _tmp ~ "/" ~ result.data.filter!isAlphaNum().to!string(); return f; } } @@ -319,7 +342,7 @@ struct ESVApiOptions bool[string] boolOpts; int[string] intOpts; string indent_using; - void setDefaults() nothrow @safe + @safe void defaults() nothrow { boolOpts["include_passage_references"] = true; boolOpts["include_verse_numbers"] = true; @@ -333,19 +356,19 @@ struct ESVApiOptions boolOpts["include_heading_horizontal_lines"] = false; boolOpts["include_selahs"] = true; boolOpts["indent_poetry"] = true; - intOpts["horizontal_line_length"] = 55; - intOpts["indent_paragraphs"] = 2; - intOpts["indent_poetry_lines"] = 4; - intOpts["indent_declares"] = 40; - intOpts["indent_psalm_doxology"] = 30; - intOpts["line_length"] = 0; - indent_using = "space"; + intOpts["horizontal_line_length"] = 55; + intOpts["indent_paragraphs"] = 2; + intOpts["indent_poetry_lines"] = 4; + intOpts["indent_declares"] = 40; + intOpts["indent_psalm_doxology"] = 30; + intOpts["line_length"] = 0; + indent_using = "space"; } } -class EsvException : Exception +class ESVException : Exception { - this(string msg, string file = __FILE__, size_t line = __LINE__) @safe pure + @safe this(string msg, string file = __FILE__, size_t line = __LINE__) pure { super(msg, file, line); } diff --git a/main.d b/main.d index 624f8bd..c1111b3 100644 --- a/main.d +++ b/main.d @@ -33,7 +33,7 @@ import dini; enum VERSION = "0.2.0"; -enum DEFAULT_APIKEY = "abfb7456fa52ec4292c79e435890cfa3df14dc2b"; // crossway approved ;) +enum DEFAULT_APIKEY = "abfb7456fa52ec4292c79e435890cfa3df14dc2b"; enum DEFAULT_CONFIGPATH = "~/.config/esv.conf"; enum DEFAULT_MPEGPLAYER = "mpg123"; enum DEFAULT_PAGER = "less"; @@ -158,14 +158,15 @@ key = " ~ DEFAULT_APIKEY ~ " panic(e.msg); } string apiKey; - try apiKey = iniData["api"].getKey("key"); + try + apiKey = iniData["api"].getKey("key"); catch (IniException e) panic("API key not present in configuration file; cannot proceed"); if (apiKey == "") panic("API key not present in configuration file; cannot proceed"); // Initialise API object and validate the book and verse - ESVApi esv = new ESVApi(apiKey); + ESVApi esv = new ESVApi(apiKey, optAudio); if (!esv.validateBook(args[1].extractBook())) panic("book '" ~ args[1] ~ "' does not exist"); if (!esv.validateVerse(args[2])) @@ -179,10 +180,12 @@ key = " ~ DEFAULT_APIKEY ~ " } else { string tmpf = esv.getAudioVerses(args[1], args[2]); string mpegPlayer = environment.get(ENV_PLAYER, DEFAULT_MPEGPLAYER); - // 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 + /* + * 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 + */ if (mpegPlayer == "mpg123") mpegPlayer = mpegPlayer ~ " -q "; else if (mpegPlayer == "mpv") @@ -191,8 +194,8 @@ key = " ~ DEFAULT_APIKEY ~ " mpegPlayer = DEFAULT_MPEGPLAYER ~ " "; // spawn mpg123 executeShell(mpegPlayer ~ tmpf); - return 0; } + return 0; } esv.extraParameters = iniData["api"].getKey("parameters"); @@ -219,7 +222,8 @@ key = " ~ DEFAULT_APIKEY ~ " } catch (IniException e) {} // just do nothing; use the default settings } // Get line_length ([passage]) - try esv.opts.intOpts["line_length"] = returnValid("0", iniData["passage"].getKey("line_length")).to!int(); + try + esv.opts.intOpts["line_length"] = returnValid("0", iniData["passage"].getKey("line_length")).to!int(); catch (ConvException e) { panic(configPath ~ ": value '" ~ iniData["passage"].getKey("line_length") ~ "' is not convertible to an integer value; must be a non-decimal number"); From 1b11097ba0712556dfa6b695776c6fccff838c9d Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Sat, 10 Jun 2023 11:41:41 +1200 Subject: [PATCH 020/133] Reorder method attributes and add pure attribute to some methods --- esvapi.d | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/esvapi.d b/esvapi.d index ca8ae44..573a71b 100644 --- a/esvapi.d +++ b/esvapi.d @@ -141,14 +141,14 @@ class ESVApi * Returns the API authentication key that was given when the API object was instantiated. * This authentication key cannot be changed after instantiation. */ - @nogc @property @safe string key() const nothrow + @property string key() const nothrow pure @nogc @safe { return _key; } /* * Returns the API authentication key currently in use. */ - @nogc @property @safe int mode() const nothrow + @property int mode() const nothrow pure @nogc @safe { return _mode; } @@ -158,7 +158,7 @@ class ESVApi * If the mode argument is not one of those, * throws an ESVException. */ - @property @safe void mode(immutable(int) mode) + @property void mode(immutable(int) mode) pure @safe { if (mode == ESVMode.TEXT || mode == ESVMode.AUDIO) _mode = mode; @@ -168,7 +168,7 @@ class ESVApi /* * Returns the API URL currently in use. */ - @nogc @property @safe string url() const nothrow + @property string url() const nothrow pure @nogc @safe { return _url; } @@ -176,7 +176,7 @@ class ESVApi * If the url argument is a valid HTTP URL, sets the API URL currently in use * to the given url argument. Otherwise, throws an ESVException. */ - @property @safe void url(immutable(string) url) + @property void url(immutable(string) url) @safe { if (url.matchAll("^https?://.+\\..+(/.+)?").empty) throw new ESVException("Invalid URL format"); @@ -186,14 +186,14 @@ class ESVApi /* * Returns the temp directory name. */ - @property @safe tmpName() const + @property tmpName() const @safe { return _tmp.replaceFirst(regex('^' ~ tempDir()), ""); } /* * Sets the temp directory name to the given string. */ - @property @safe void tmpName(immutable(string) name) + @property void tmpName(immutable(string) name) @safe { _tmp = tempDir() ~ name; } @@ -201,7 +201,7 @@ class ESVApi * Returns true if the argument book is a valid book of the Bible. * Otherwise, returns false. */ - @safe bool validateBook(in char[] book) const nothrow + bool validateBook(in char[] book) const nothrow @safe { foreach (b; BIBLE_BOOKS) { if (book.capitalize() == b.capitalize()) @@ -213,9 +213,9 @@ class ESVApi * Returns true if the argument book is a valid verse format. * Otherwise, returns false. */ - @safe bool validateVerse(in char[] verse) const + bool validateVerse(in char[] verse) const @safe { - @safe bool attemptRegex(string re) const + bool attemptRegex(string re) const @safe { return !verse.matchAll(re).empty; } @@ -288,7 +288,7 @@ class ESVApi request.onProgress = onProgress; request.onReceive = (ubyte[] data) { - response = response ~= data; + response ~= data; return data.length; }; request.addRequestHeader("Authorization", "Token " ~ _key); @@ -298,10 +298,10 @@ class ESVApi return tmpFile; } private: - @safe string assembleParameters() const + string assembleParameters() const pure @safe { string params = ""; - string addParam(string param, string value) const + string addParam(string param, string value) const pure @safe { return format!"%s&%s=%s"(params, param, value); } @@ -326,7 +326,7 @@ class ESVApi params = addParam("indent-using", opts.indent_using.to!string); return params; } - @safe string tempFile() const + string tempFile() const @safe { auto rndNums = rndGen().map!(a => cast(ubyte)a)().take(32); auto result = appender!string(); @@ -342,7 +342,7 @@ struct ESVApiOptions bool[string] boolOpts; int[string] intOpts; string indent_using; - @safe void defaults() nothrow + void defaults() nothrow @safe { boolOpts["include_passage_references"] = true; boolOpts["include_verse_numbers"] = true; From 06a4dc286d90717a5695f90655c0ce46a8a3882f Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Sat, 23 Sep 2023 17:10:38 +1200 Subject: [PATCH 021/133] Refactor most of the code - Replace panic()-based error handling in main() with Exception-based error handling - Put hardcoded defaults in config.di - Make general optimisations and readability improvements for a lot of the code - Change some of the ESVApi's interface --- config.di | 18 +++ esvapi.d | 410 ++++++++++++++++++++++++++++-------------------------- main.d | 331 +++++++++++++++++++++---------------------- 3 files changed, 397 insertions(+), 362 deletions(-) create mode 100644 config.di diff --git a/config.di b/config.di new file mode 100644 index 0000000..e40261f --- /dev/null +++ b/config.di @@ -0,0 +1,18 @@ +/* default configuration for esv */ + +module config; + +public: + +enum DEFAULT_APIKEY = "abfb7456fa52ec4292c79e435890cfa3df14dc2b"; +enum DEFAULT_CONFIGPATH = "~/.config/esv.conf"; +enum DEFAULT_MPEGPLAYER = "mpg123"; +enum DEFAULT_PAGER = "less"; + +enum ENV_CONFIG = "ESV_CONFIG"; +enum ENV_PAGER = "ESV_PAGER"; +enum ENV_PLAYER = "ESV_PLAYER"; + +enum BUGREPORTURL = "https://codeberg.org/jtbx/esv/issues"; + +// vi: ft=d diff --git a/esvapi.d b/esvapi.d index 573a71b..84d17a7 100644 --- a/esvapi.d +++ b/esvapi.d @@ -20,31 +20,25 @@ module esvapi; -import std.algorithm : filter, map; -import std.array : appender; -import std.ascii : isAlphaNum; -import std.base64 : Base64; import std.conv : to; -import std.file : mkdirRecurse, tempDir, write; +import std.exception : basicExceptionCtors, enforce; +import std.file : tempDir, write; import std.format : format; import std.json : JSONValue, parseJSON; -import std.random : rndGen; -import std.range : take; -import std.regex : matchAll, replaceAll, replaceFirst, regex; +import std.regex : regex, matchAll, replaceAll; +import std.stdio : File; import std.string : capitalize; -import std.utf : toUTF8; -import std.net.curl; +import std.net.curl : HTTP; -public enum ESVMode +enum ESVIndent { - TEXT, - AUDIO + SPACE, + TAB } -const enum ESVAPI_KEY = "abfb7456fa52ec4292c79e435890cfa3df14dc2b"; -const enum ESVAPI_URL = "https://api.esv.org/v3/passage"; +const enum string ESVAPI_URL = "https://api.esv.org/v3/passage"; const string[] BIBLE_BOOKS = [ - // Old Testament + /* Old Testament */ "Genesis", "Exodus", "Leviticus", @@ -64,7 +58,7 @@ const string[] BIBLE_BOOKS = [ "Esther", "Job", "Psalm", - "Psalms", // both are valid + "Psalms", /* both are valid */ "Proverbs", "Ecclesiastes", "Song of Solomon", @@ -85,7 +79,7 @@ const string[] BIBLE_BOOKS = [ "Haggai", "Zechariah", "Malachi", - // New Testament + /* New Testament */ "Matthew", "Mark", "Luke", @@ -114,154 +108,167 @@ const string[] BIBLE_BOOKS = [ "Jude", "Revelation" ]; +/* All boolean API parameters */ +const string[] ESVAPI_PARAMETERS = [ + "include-passage-references", + "include-verse-numbers", + "include-first-verse-numbers", + "include-footnotes", + "include-footnote-body", + "include-headings", + "include-short-copyright", + "include-copyright", + "include-passage-horizontal-lines", + "include-heading-horizontal-lines", + "include-selahs", + "indent-poetry", + "horizontal-line-length", + "indent-paragraphs", + "indent-poetry-lines", + "indent-declares", + "indent-psalm-doxology", + "line-length", + "indent-using", +]; + +/* + * Returns true if the argument book is a valid book of the Bible. + * Otherwise, returns false. + */ +bool bookValid(in char[] book) nothrow @safe +{ + foreach (string b; BIBLE_BOOKS) { + if (book.capitalize() == b.capitalize()) + return true; + } + return false; +} +/* + * Returns true if the argument book is a valid verse format. + * Otherwise, returns false. + */ +bool verseValid(in char[] verse) @safe +{ + bool vMatch(in string re) @safe + { + return !verse.matchAll(regex(re)).empty; + } + if (vMatch("^\\d{1,3}$") || + vMatch("^\\d{1,3}-\\d{1,3}$") || + vMatch("^\\d{1,3}:\\d{1,3}$") || + vMatch("^\\d{1,3}:\\d{1,3}-\\d{1,3}$")) + return true; + + return false; +} + +} class ESVApi { - private { - int _mode; + protected { string _key; string _tmp; string _url; } - ESVApiOptions opts; + string extraParameters; int delegate(size_t, size_t, size_t, size_t) onProgress; - this(immutable(string) key = ESVAPI_KEY, bool audio = false) + ESVApiOptions opts; + + this(string key, string tmpName = "esv") { - _key = key; - _mode = audio ? ESVMode.AUDIO : ESVMode.TEXT; - _tmp = tempDir() ~ "esv"; - _url = ESVAPI_URL; - opts.defaults(); + _key = key; + _tmp = tempDir() ~ tmpName; + _url = ESVAPI_URL; + opts = ESVApiOptions(true); extraParameters = ""; onProgress = (size_t dlTotal, size_t dlNow, size_t ulTotal, size_t ulNow) { return 0; }; - tmpName = "esv"; } + /* - * Returns the API authentication key that was given when the API object was instantiated. - * This authentication key cannot be changed after instantiation. + * Returns the API authentication key that was given when the object + * was constructed. This authentication key cannot be changed. */ - @property string key() const nothrow pure @nogc @safe + @property string key() const nothrow pure @safe { return _key; } /* - * Returns the API authentication key currently in use. + * Returns the subdirectory used to store temporary audio passages. */ - @property int mode() const nothrow pure @nogc @safe + @property string tmpDir() const nothrow pure @safe { - return _mode; - } - /* - * If the mode argument is either "text" or "html", - * sets the text API mode to the given mode argument. - * If the mode argument is not one of those, - * throws an ESVException. - */ - @property void mode(immutable(int) mode) pure @safe - { - if (mode == ESVMode.TEXT || mode == ESVMode.AUDIO) - _mode = mode; - else - throw new ESVException("Invalid mode"); + return _tmp; } /* * Returns the API URL currently in use. */ - @property string url() const nothrow pure @nogc @safe + @property string url() const nothrow pure @safe { return _url; } /* - * If the url argument is a valid HTTP URL, sets the API URL currently in use - * to the given url argument. Otherwise, throws an ESVException. + * Sets the API URL currently in use to the given url argument. */ @property void url(immutable(string) url) @safe + in (!url.matchAll(`^https?://.+\\..+(/.+)?`).empty, "Invalid URL format") { - if (url.matchAll("^https?://.+\\..+(/.+)?").empty) - throw new ESVException("Invalid URL format"); - else - _url = url; + _url = url; } /* - * Returns the temp directory name. - */ - @property tmpName() const @safe - { - return _tmp.replaceFirst(regex('^' ~ tempDir()), ""); - } - /* - * Sets the temp directory name to the given string. - */ - @property void tmpName(immutable(string) name) @safe - { - _tmp = tempDir() ~ name; - } - /* - * Returns true if the argument book is a valid book of the Bible. - * Otherwise, returns false. - */ - bool validateBook(in char[] book) const nothrow @safe - { - foreach (b; BIBLE_BOOKS) { - if (book.capitalize() == b.capitalize()) - return true; - } - return false; - } - /* - * Returns true if the argument book is a valid verse format. - * Otherwise, returns false. - */ - bool validateVerse(in char[] verse) const @safe - { - bool attemptRegex(string re) const @safe - { - return !verse.matchAll(re).empty; - } - if (attemptRegex("^\\d{1,3}$") || - attemptRegex("^\\d{1,3}-\\d{1,3}$") || - attemptRegex("^\\d{1,3}:\\d{1,3}$") || - attemptRegex("^\\d{1,3}:\\d{1,3}-\\d{1,3}$")) - return true; - else - return false; - } - /* - * Requests the verse(s) from the API and returns it. + * Requests the verse(s) in text format from the API and returns it. * The (case-insensitive) name of the book being searched are * contained in the argument book. The verse(s) being looked up are * contained in the argument verses. * - * If the mode is ESVMode.AUDIO, requests an audio passage instead. - * A file path to an MP3 audio track is returned. - * To explicitly get an audio passage without setting the mode, - * use getAudioVerses(). - * * Example: getVerses("John", "3:16-21") */ string getVerses(in char[] book, in char[] verse) const + in (bookValid(book), "Invalid book") + in (verseValid(verse), "Invalid verse format") { - if (_mode == ESVMode.AUDIO) { - return getAudioVerses(book, verse); + char[] params, response; + HTTP request; + + params = []; + + { + (char[])[] parambuf; + foreach (string opt; ESVAPI_PARAMETERS) { + bool bo; + int io; + + switch (opt) { + case "indent-using": + o = opts.indent_using; + break; + case "indent-poetry": + } + !opt.matchAll("^include-").empty) { + o = opts.b[opt]; + } else if (opt == "line-length" || + opt == "horizontal-line-length" || + !opt.matchAll("^indent-").empty) { + o = opts.i[opt]; + } + params = format!"%s&%s=%s"(params, opt, o.to!string()); + } } - if (!validateBook(book)) - throw new ESVException("Invalid book"); - if (!validateVerse(verse)) - throw new ESVException("Invalid verse format"); - - string apiURL = format!"%s/%s/?q=%s+%s%s%s"(_url, _mode, - book.capitalize().replaceAll(regex(" "), "+"), verse, - assembleParameters(), extraParameters); - auto request = HTTP(apiURL); - string response; + request = HTTP( + format!"%s/text/?q=%s+%s%s%s"(_url, book + .capitalize() + .replaceAll(regex(" "), "+"), + verse, params, extraParameters) + ); request.onProgress = onProgress; - request.onReceive = (ubyte[] data) - { - response = cast(string)data; - return data.length; - }; + request.onReceive = + (ubyte[] data) + { + response ~= data; + return data.length; + }; request.addRequestHeader("Authorization", "Token " ~ _key); request.perform(); return response.parseJSON()["passages"][0].str; @@ -273,103 +280,110 @@ class ESVApi * contained in the argument book. The verse(s) being looked up are * contained in the argument verses. * - * Example: getVerses("John", "3:16-21") + * Example: getAudioVerses("John", "3:16-21") */ string getAudioVerses(in char[] book, in char[] verse) const + in (bookValid(book), "Invalid book") + in (verseValid(verse), "Invalid verse format") { - if (!validateBook(book)) - throw new ESVException("Invalid book"); - if (!validateVerse(verse)) - throw new ESVException("Invalid verse format"); + char[] response; + File tmpFile; - string apiURL = format!"%s/audio/?q=%s+%s"(_url, book.capitalize().replaceAll(regex(" "), "+"), verse); - auto request = HTTP(apiURL); - ubyte[] response; + auto request = HTTP(format!"%s/audio/?q=%s+%s"( + _url, book.capitalize().replaceAll(regex(" "), "+"), verse)); request.onProgress = onProgress; - request.onReceive = (ubyte[] data) - { - response ~= data; - return data.length; - }; + request.onReceive = + (ubyte[] data) + { + response ~= data; + return data.length; + }; request.addRequestHeader("Authorization", "Token " ~ _key); request.perform(); - string tmpFile = tempFile(); + tmpFile = File(_tmp, "w"); tmpFile.write(response); - return tmpFile; + return _tmp; } - private: - string assembleParameters() const pure @safe + /* + * Requests a passage search for the given query. + * If raw is false, formats the passage search as + * plain text, otherwise returns JSON from the API. + * + * Example: search("It is finished") + */ + char[] search(in string query, in bool raw = true) { - string params = ""; - string addParam(string param, string value) const pure @safe - { - return format!"%s&%s=%s"(params, param, value); + ulong i; + char[] response; + char[] layout; + HTTP request; + JSONValue json; + + request = HTTP(format!"%s/search/?q=%s"( + _url, query.replaceAll(regex(" "), "+"))); + request.onProgress = onProgress; + request.onReceive = + (ubyte[] data) + { + response ~= cast(char[])data; + return data.length; + }; + request.addRequestHeader("Authorization", "Token " ~ _key); + request.perform(); + + if (raw) + return response; + + json = response.parseJSON(); + layout = cast(char[])""; + + enforce!ESVException(json["total_results"].integer == 0, "No results for search"); + + foreach (ulong i, JSONValue result; json["results"]) { + layout ~= format!"%s\n %s\n"( + result["reference"].str, + result["content"].str + ); } - params = addParam("include-passage-references", opts.boolOpts["include_passage_references"].to!string); - params = addParam("include-verse-numbers", opts.boolOpts["include_verse_numbers"].to!string); - params = addParam("include-first-verse-numbers", opts.boolOpts["include_first_verse_numbers"].to!string); - params = addParam("include-footnotes", opts.boolOpts["include_footnotes"].to!string); - params = addParam("include-footnote-body", opts.boolOpts["include_footnote_body"].to!string); - params = addParam("include-headings", opts.boolOpts["include_headings"].to!string); - params = addParam("include-short-copyright", opts.boolOpts["include_short_copyright"].to!string); - params = addParam("include-copyright", opts.boolOpts["include_copyright"].to!string); - params = addParam("include-passage-horizontal-lines", opts.boolOpts["include_passage_horizontal_lines"].to!string); - params = addParam("include-heading-horizontal-lines", opts.boolOpts["include_heading_horizontal_lines"].to!string); - params = addParam("include-selahs", opts.boolOpts["include_selahs"].to!string); - params = addParam("indent-poetry", opts.boolOpts["indent_poetry"].to!string); - params = addParam("horizontal-line-length", opts.intOpts ["horizontal_line_length"].to!string); - params = addParam("indent-paragraphs", opts.intOpts ["indent_paragraphs"].to!string); - params = addParam("indent-poetry-lines", opts.intOpts ["indent_poetry_lines"].to!string); - params = addParam("indent-declares", opts.intOpts ["indent_declares"].to!string); - params = addParam("indent-psalm-doxology", opts.intOpts ["indent_psalm_doxology"].to!string); - params = addParam("line-length", opts.intOpts ["line_length"].to!string); - params = addParam("indent-using", opts.indent_using.to!string); - return params; - } - string tempFile() const @safe - { - auto rndNums = rndGen().map!(a => cast(ubyte)a)().take(32); - auto result = appender!string(); - Base64.encode(rndNums, result); - _tmp.mkdirRecurse(); - string f = _tmp ~ "/" ~ result.data.filter!isAlphaNum().to!string(); - return f; + + return layout; } } struct ESVApiOptions { - bool[string] boolOpts; - int[string] intOpts; - string indent_using; - void defaults() nothrow @safe + bool[string] b; + int[string] i; + ESVIndent indent_using; + + this(bool initialise) nothrow @safe { - boolOpts["include_passage_references"] = true; - boolOpts["include_verse_numbers"] = true; - boolOpts["include_first_verse_numbers"] = true; - boolOpts["include_footnotes"] = true; - boolOpts["include_footnote_body"] = true; - boolOpts["include_headings"] = true; - boolOpts["include_short_copyright"] = true; - boolOpts["include_copyright"] = false; - boolOpts["include_passage_horizontal_lines"] = false; - boolOpts["include_heading_horizontal_lines"] = false; - boolOpts["include_selahs"] = true; - boolOpts["indent_poetry"] = true; - intOpts["horizontal_line_length"] = 55; - intOpts["indent_paragraphs"] = 2; - intOpts["indent_poetry_lines"] = 4; - intOpts["indent_declares"] = 40; - intOpts["indent_psalm_doxology"] = 30; - intOpts["line_length"] = 0; - indent_using = "space"; + if (!initialise) + return; + + b["include-passage-references"] = true; + b["include-verse-numbers"] = true; + b["include-first-verse-numbers"] = true; + b["include-footnotes"] = true; + b["include-footnote-body"] = true; + b["include-headings"] = true; + b["include-short-copyright"] = true; + b["include-copyright"] = false; + b["include-passage-horizontal-lines"] = false; + b["include-heading-horizontal-lines"] = false; + b["include-selahs"] = true; + b["indent-poetry"] = true; + i["horizontal-line-length"] = 55; + i["indent-paragraphs"] = 2; + i["indent-poetry-lines"] = 4; + i["indent-declares"] = 40; + i["indent-psalm-doxology"] = 30; + i["line-length"] = 0; + indent_using = ESVIndent.TAB; } } class ESVException : Exception { - @safe this(string msg, string file = __FILE__, size_t line = __LINE__) pure - { - super(msg, file, line); - } + mixin basicExceptionCtors; } diff --git a/main.d b/main.d index c1111b3..56686d1 100644 --- a/main.d +++ b/main.d @@ -19,123 +19,139 @@ */ import std.conv : to, ConvException; -import std.file : exists, write, FileException; +import std.exception : enforce; +import std.file : exists, mkdirRecurse, write, FileException; import std.format : format; -import std.getopt : getopt, GetOptException, config; -import std.path : baseName, expandTilde, isValidPath; +import std.getopt : getopt, GetOptException; +import std.getopt : getoptConfig = config; +import std.path : baseName, dirName, expandTilde, isValidPath; import std.process : environment, executeShell; import std.regex : regex, matchFirst, replaceAll, replaceFirst; import std.stdio : writef, writeln, writefln, stderr; import std.string : splitLines; +import config; import esvapi; import dini; enum VERSION = "0.2.0"; -enum DEFAULT_APIKEY = "abfb7456fa52ec4292c79e435890cfa3df14dc2b"; -enum DEFAULT_CONFIGPATH = "~/.config/esv.conf"; -enum DEFAULT_MPEGPLAYER = "mpg123"; -enum DEFAULT_PAGER = "less"; +bool aFlag; /* audio */ +string CFlag; /* config */ +bool fFlag, FFlag; /* footnotes */ +bool hFlag, HFlag; /* headings */ +int lFlag; /* line length */ +bool nFlag, NFlag; /* verse numbers */ +bool PFlag; /* disable pager */ +bool rFlag, RFlag; /* passage references */ +bool VFlag; /* show version */ -enum ENV_CONFIG = "ESV_CONFIG"; -enum ENV_PAGER = "ESV_PAGER"; -enum ENV_PLAYER = "ESV_PLAYER"; - -bool optAudio; -string optConfigPath; -bool optFootnotes; -bool optNoFootnotes; -bool optHeadings; -bool optNoHeadings; -int optLineLength = 0; -bool optVerseNumbers; -bool optNoVerseNumbers; -bool optNoPager; -bool optPassageReferences; -bool optNoPassageReferences; -bool optVersion; - -int main(string[] args) +int +main(string[] args) { - void msg(string s) - { - stderr.writefln("%s: %s", args[0].baseName(), s); + bool success; + + debug { + return run(args); } - void panic(string s) - { - import core.runtime : Runtime; - import core.stdc.stdlib : exit; - - msg(s); - scope(exit) { - Runtime.terminate(); - exit(1); + try { + success = run(args); + } catch (Exception e) { + if (typeid(e) == typeid(Exception)) { + stderr.writefln("%s: %s", args[0].baseName(), e.msg); + } else { + stderr.writefln("%s: uncaught %s in %s:%d: %s", + args[0].baseName(), + typeid(e).name, + e.file, e.line, e.msg); + stderr.writefln("this is probably a bug; it would be greatly appreciated if you reported it at:\n\n %s", + BUGREPORTURL); } } - // Parse command-line options + return success ? 0 : 1; +} + +bool +run(string[] args) +{ + ushort lines; + string apiKey; + string configPath; + string configEnvVar; + string pager; + string verses; + Ini iniData; + ESVApi esv; + + /* Parse command-line options */ try { args.getopt( - config.bundling, - config.caseSensitive, - "a", &optAudio, - "C", &optConfigPath, - "F", &optNoFootnotes, - "f", &optFootnotes, - "H", &optNoHeadings, - "h", &optHeadings, - "l", &optLineLength, - "N", &optNoVerseNumbers, - "n", &optVerseNumbers, - "P", &optNoPager, - "R", &optNoPassageReferences, - "r", &optNoPassageReferences, - "V", &optVersion, + getoptConfig.bundling, + getoptConfig.caseSensitive, + "a", &aFlag, + "C", &CFlag, + "F", &FFlag, + "f", &fFlag, + "H", &HFlag, + "h", &hFlag, + "l", &lFlag, + "N", &NFlag, + "n", &nFlag, + "P", &PFlag, + "R", &RFlag, + "r", &rFlag, + "V", &VFlag, ); } catch (GetOptException e) { - if (!e.msg.matchFirst(regex("^Unrecognized option")).empty) - panic("unknown option " ~ e.extractOpt()); - else if (!e.msg.matchFirst(regex("^Missing value for argument")).empty) - panic("missing argument for option " ~ e.extractOpt()); - } catch (ConvException e) - panic("value provided by option -l is not convertible to an integer value; must be a non-decimal number"); + enforce(e.msg.matchFirst(regex("^Unrecognized option")).empty, + "unknown option " ~ e.extractOpt()); + enforce(e.msg.matchFirst(regex("^Missing value for argument")).empty, + "missing argument for option " ~ e.extractOpt()); - if (optVersion) { + throw new Exception(e.msg); /* catch-all */ + } catch (ConvException e) { + throw new Exception( + "illegal argument to -l option -- integer required"); + } + + if (VFlag) { writeln("esv version " ~ VERSION); - return 0; + return true; } if (args.length < 3) { stderr.writefln("usage: %s [-C config] [-l length] [-aFfHhNnPRrV] book verses", args[0].baseName()); - return 1; + return false; } - // Determine configuration file - // Options have first priority, then environment variables, then the default path - string configPath; - string configEnvVar = environment.get(ENV_CONFIG); - Ini iniData; + enforce(bookValid(args[1].parseBook()), + format!"book '%s' does not exist"(args[1])); + enforce(verseValid(args[2]), + format!"invalid verse format '%s'"(args[2])); + + /* determine configuration file + * Options have first priority, then environment variables, + * then the default path */ + configPath = environment.get(ENV_CONFIG, DEFAULT_CONFIGPATH) + .expandTilde(); try { - if (optConfigPath != "") { - if (optConfigPath.isValidPath()) - configPath = optConfigPath.expandTilde(); - else - panic(optConfigPath ~ ": invalid file path"); + if (CFlag != "") { /* if -C was given */ + enforce(isValidPath(CFlag), CFlag ~ ": invalid path"); + configPath = CFlag.expandTilde(); } else { - configPath = environment.get(ENV_CONFIG, DEFAULT_CONFIGPATH); - if (configPath.isValidPath()) - configPath = configPath.expandTilde(); - else - panic(configEnvVar ~ ": invalid file path"); + enforce(isValidPath(configPath), + configPath ~ ": invalid path"); + if (!configPath.exists()) { - configPath.write( + mkdirRecurse(configPath.dirName()); + configPath.write(format! "# Default esv configuration file. # An API key is required to access the ESV Bible API. [api] -key = " ~ DEFAULT_APIKEY ~ " +key = %s # If you really need to, you can specify # custom API parameters using `parameters`: #parameters = &my-parameter=value @@ -146,136 +162,123 @@ key = " ~ DEFAULT_APIKEY ~ " #headings = false #passage_references = false #verse_numbers = false -"); +"(DEFAULT_APIKEY)); } } iniData = Ini.Parse(configPath); } catch (FileException e) { - // filesystem syscall errors - if (!e.msg.matchFirst(regex("^" ~ configPath ~ ": [Ii]s a directory")).empty || - !e.msg.matchFirst(regex("^" ~ configPath ~ ": [Nn]o such file or directory")).empty || - !e.msg.matchFirst(regex("^" ~ configPath ~ ": [Pp]ermission denied")).empty) - panic(e.msg); + /* filesystem syscall errors */ + throw new Exception(e.msg); } - string apiKey; - try + try { apiKey = iniData["api"].getKey("key"); - catch (IniException e) - panic("API key not present in configuration file; cannot proceed"); - if (apiKey == "") - panic("API key not present in configuration file; cannot proceed"); + } catch (IniException e) { + apiKey = ""; + } + enforce(apiKey != "", + "API key not present in configuration file; cannot proceed"); - // Initialise API object and validate the book and verse - ESVApi esv = new ESVApi(apiKey, optAudio); - if (!esv.validateBook(args[1].extractBook())) - panic("book '" ~ args[1] ~ "' does not exist"); - if (!esv.validateVerse(args[2])) - panic("invalid verse format '" ~ args[2] ~ "'"); + esv = new ESVApi(apiKey); - if (optAudio) { - // check for mpg123 - if (executeShell("which " ~ DEFAULT_MPEGPLAYER ~ " >/dev/null 2>&1").status > 0) { - panic(DEFAULT_MPEGPLAYER ~ " is required for audio mode; cannot continue"); - return 1; - } else { - string tmpf = esv.getAudioVerses(args[1], args[2]); - string mpegPlayer = environment.get(ENV_PLAYER, DEFAULT_MPEGPLAYER); - /* - * 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 - */ - if (mpegPlayer == "mpg123") - mpegPlayer = mpegPlayer ~ " -q "; - else if (mpegPlayer == "mpv") - mpegPlayer = mpegPlayer ~ " --msg-level=all=no "; - else - mpegPlayer = DEFAULT_MPEGPLAYER ~ " "; - // spawn mpg123 - executeShell(mpegPlayer ~ tmpf); - } - return 0; + if (aFlag) { + string tmpf, mpegPlayer; + + /* check for mpg123 */ + enforce(executeShell( + format!"command -v %s >/dev/null 2>&1"(DEFAULT_MPEGPLAYER)).status == 0, + DEFAULT_MPEGPLAYER ~ " is required for audio mode; cannot continue"); + + tmpf = esv.getAudioVerses(args[1], args[2]); + mpegPlayer = environment.get(ENV_PLAYER, DEFAULT_MPEGPLAYER); + /* 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 */ + mpegPlayer ~= + mpegPlayer == "mpg123" ? " -q " : + mpegPlayer == "mpv" ? " --msg-level=all=no " : " "; + /* spawn mpg123 */ + executeShell(mpegPlayer ~ tmpf); + return true; } esv.extraParameters = iniData["api"].getKey("parameters"); string returnValid(string def, string val) { - if (val == "") - return def; - else - return val; + return val == "" ? def : val; } - // Get [passage] keys + /* Get [passage] keys */ foreach (string key; ["footnotes", "headings", "passage_references", "verse_numbers"]) { try { - esv.opts.boolOpts["include_" ~ key] = + esv.opts.b["include_" ~ key] = returnValid("true", iniData["passage"].getKey(key)).to!bool(); } catch (ConvException e) { - panic( - format!"%s: key '%s' of section 'passage' is not a boolean value (must be either 'true' or 'false')"( - configPath, key - ) + throw new Exception(format! + "%s: key '%s' of section 'passage' is not a boolean value (must be either 'true' or 'false')" + (configPath, key) ); } catch (IniException e) {} // just do nothing; use the default settings } - // Get line_length ([passage]) - try - esv.opts.intOpts["line_length"] = returnValid("0", iniData["passage"].getKey("line_length")).to!int(); + /* Get line_length ([passage]) */ + try esv.opts.i["line_length"] = returnValid("0", iniData["passage"].getKey("line_length")).to!int(); catch (ConvException e) { - panic(configPath ~ ": value '" ~ iniData["passage"].getKey("line_length") - ~ "' is not convertible to an integer value; must be a non-decimal number"); + throw new Exception( + format!"%s: illegal value '%s' -- must be an integer"( + configPath, + iniData["passage"].getKey("line_length")) + ); } catch (IniException e) {} // just do nothing; use the default setting - if (optFootnotes) esv.opts.boolOpts["include_footnotes"] = true; - if (optHeadings) esv.opts.boolOpts["include_headings"] = true; - if (optVerseNumbers) esv.opts.boolOpts["include_verse_numbers"] = true; - if (optPassageReferences) esv.opts.boolOpts["include_passage_references"] = true; - if (optNoFootnotes) esv.opts.boolOpts["include_footnotes"] = false; - if (optNoHeadings) esv.opts.boolOpts["include_headings"] = false; - if (optNoVerseNumbers) esv.opts.boolOpts["include_verse_numbers"] = false; - if (optNoPassageReferences) esv.opts.boolOpts["include_passage_references"] = false; - if (optLineLength != 0) esv.opts.intOpts ["line_length"] = optLineLength; + 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; + if (lFlag != 0) esv.opts.i["line_length"] = lFlag; - string verses = esv.getVerses(args[1].extractBook(), args[2]); - int lines; + verses = esv.getVerses(args[1].parseBook(), args[2]); foreach (string line; verses.splitLines()) ++lines; - // If the passage is very long, pipe it into a pager - if (lines > 32 && !optNoPager) { + /* If the passage is very long, pipe it into a pager */ + if (lines > 32 && !PFlag) { import std.process : pipeProcess, Redirect, wait, ProcessException; - string pager = environment.get(ENV_PAGER, DEFAULT_PAGER); + + pager = environment.get(ENV_PAGER, DEFAULT_PAGER); try { auto pipe = pipeProcess(pager, Redirect.stdin); pipe.stdin.writeln(verses); pipe.stdin.flush(); pipe.stdin.close(); pipe.pid.wait(); - } - catch (ProcessException e) { - if (!e.msg.matchFirst(regex("^Executable file not found")).empty) { - panic(e.msg + } catch (ProcessException e) { + enforce(e.msg.matchFirst(regex("^Executable file not found")).empty, + format!"%s: command not found"(e.msg .matchFirst(": (.+)$")[0] .replaceFirst(regex("^: "), "") - ~ ": command not found" - ); - } - } - } else - writeln(verses); + )); - return 0; + throw new Exception(e.msg); /* catch-all */ + } + + return true; + } + + writeln(verses); + return true; } -string extractOpt(in GetOptException e) @safe +private string extractOpt(in GetOptException e) @safe { return e.msg.matchFirst("-.")[0]; } -string extractBook(in string book) @safe +private string parseBook(in string book) @safe { return book.replaceAll(regex("[-_]"), " "); } From 7ebc0d7b660e8a6598618009684862d99b295551 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Sat, 23 Sep 2023 17:19:21 +1200 Subject: [PATCH 022/133] Rename main.d -> esv.d --- main.d => esv.d | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename main.d => esv.d (100%) diff --git a/main.d b/esv.d similarity index 100% rename from main.d rename to esv.d From 6efe117545cbceef4368866678f1c9e049e2b301 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Sun, 24 Sep 2023 10:31:12 +1300 Subject: [PATCH 023/133] Switch to configure and make based build Also removed excessive use of std.regex in esvapi.d and made it actually compile -_- --- .gitignore | 1 + Makefile | 32 --------- README.md | 35 ++++++---- configure | 187 +++++++++++++++++++++++++++++++++++++++++++++++++++++ esvapi.d | 67 ++++++++++++------- 5 files changed, 254 insertions(+), 68 deletions(-) delete mode 100644 Makefile create mode 100755 configure diff --git a/.gitignore b/.gitignore index c4d9507..ad9dd2d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ *.so *.a esv +Makefile diff --git a/Makefile b/Makefile deleted file mode 100644 index 6bd438e..0000000 --- a/Makefile +++ /dev/null @@ -1,32 +0,0 @@ -IMPORT = import -PREFIX = /usr/local -MANPREFIX = /usr/share/man - -DC = ldc2 -CFLAGS = -O -I${IMPORT} -release -w -OBJS = main.o esvapi.o ini.o - -all: esv - -esv: ${OBJS} - ${DC} ${CFLAGS} -of$@ ${OBJS} - -# main executable -main.o: main.d esvapi.o - ${DC} ${CFLAGS} -of$@ -c main.d - -esvapi.o: esvapi.d - ${DC} ${CFLAGS} -of$@ -c esvapi.d - -ini.o: ${IMPORT}/dini/*.d - ${DC} ${CFLAGS} -of$@ -c ${IMPORT}/dini/*.d - -clean: - rm -f esv ${OBJS} - -install: esv - install -m755 esv ${DESTDIR}${PREFIX}/bin/esv - cp -f esv.1 ${DESTDIR}${MANPREFIX}/man1 - cp -f esv.conf.5 ${DESTDIR}${MANPREFIX}/man5 - -.PHONY: all clean install diff --git a/README.md b/README.md index 5bfc305..d69a187 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ *Read the Bible from your terminal* -`esv` is a utility that displays passages of the English Standard Bible on your terminal. -It connects to the ESV web API to retrieve the passages, +`esv` is a utility that displays passages of the English Standard Bible +on your terminal. It connects to the ESV web API to retrieve the passages, and allows configuration through command-line options and the configuration file. Example usage: @@ -20,18 +20,20 @@ A Psalm of David. He makes me lie down in green pastures.... ``` -If the requested passage is over 32 lines long, `esv` will pipe it through a pager -(default less). The pager being used can be changed through the `ESV_PAGER` -environment variable or just disabled altogether by passing the -P option. +If the requested passage is over 32 lines long, `esv` will pipe it through +a pager (default less). The pager being used can be changed through the +`ESV_PAGER` environment variable or just disabled altogether by passing the +-P option. -The names of Bible books are not case sensitive, so John, john, JOHN and jOhN +The names of Bible books are not case sensitive, so John, john, and JOHN are all accepted. ## Audio `esv` supports playing audio passages through the -a option. -The `mpg123` audio/video player is utilised here and so it is therefore required -if you want to use audio mode. +The `mpg123` audio/video player is utilised here and so it required if you +want to play audio passages. If you prefer, you can use a different player +(such as mpv) by editing config.di. Audio usage is the same as normal text usage. `esv -a Matthew 5-7` will play an audio passage of Matthew 5-7. @@ -40,10 +42,13 @@ an audio passage of Matthew 5-7. To install `esv`, first make sure you have the [LLVM D compiler (ldc)](https://github.com/ldc-developers/ldc#installation) -installed on your system. You should also have Phobos (the D standard library, comes included with LDC) -installed as a dynamic library in order to run the executable. +installed on your system. -First clone the source code repository: +Commands prefixed with a dollar sign ($) are intended to be run as +a standard user, and commands prefixed with a hash sign (#) are intended +to be run as the root user. + +First, get the source code: ``` $ git clone https://codeberg.org/jtbx/esv @@ -53,13 +58,15 @@ $ cd esv Now, compile and install: ``` +$ ./configure $ make # make install ``` -By default the Makefile guesses that the ldc executable is named `ldc2`. If it is installed -under a different name, or if you wish to use a different compiler, use `make DC=compiler` -(where `compiler` is your compiler) instead. + ## Documentation diff --git a/configure b/configure new file mode 100755 index 0000000..8d03e11 --- /dev/null +++ b/configure @@ -0,0 +1,187 @@ +#!/usr/bin/env sh +# simple and flexible configure script for people who don't like to waste time +# licensed to the public domain + +set -e + +IMPORT=import + +mkf=Makefile +cflags=-I"$IMPORT" +objs='esv.o esvapi.o' +srcs='esv.d esvapi.d' +makefile=' +IMPORT = '"$IMPORT"' +PREFIX = /usr/local +MANPREFIX = ${PREFIX}/man + +DC = ${_DC} +CFLAGS = ${_CFLAGS} +OBJS = ${_OBJS} ini.o + +all: esv + +esv: ${OBJS} + ${DC} ${_LDFLAGS} -of=$@ ${OBJS} + +.SUFFIXES: .d .o + +.d.o: + ${DC} ${CFLAGS} -c $< + +ini.o: ${IMPORT}/dini/*.d + ${DC} ${CFLAGS} -of=ini.o -c ${IMPORT}/dini/*.d + +clean: + rm -f esv ${OBJS} + +install: esv + install -m755 esv ${DESTDIR}${PREFIX}/bin/esv + cp -f esv.1 ${DESTDIR}${MANPREFIX}/man1 + cp -f esv.conf.5 ${DESTDIR}${MANPREFIX}/man5 + +.PHONY: all clean install +' + +# utility functions + +present () { + command -v "$1" 1>/dev/null 2>/dev/null +} +using () { + >&2 printf "using $1\n" +} +error () { + >&2 printf "$(basename $0): $1\n" + exit 1 +} + +# generators + +## D compiler +gen_DC () { + if ! [ -z "$dc" ]; then + using "$dc" + return 0 + fi + if present ldc2; then + dc=ldc2 + using ldc2 + elif present dmd; then + dc=dmd + using dmd + else + error "D compiler not found; install ldc or dmd" + fi +} + +## flags used in the compilation step +gen_CFLAGS () { + if [ -z "$debug" ]; then + case "$dc" in + ldc2) cflags="-Oz";; + dmd) cflags="-O";; + esac + using "$cflags" + else + fdebugsymbols="-g" + using "$fdebugsymbols" + case "$dc" in + ldc2) + fdebug="-d-debug" + using "$fdebug" + foptimisation="-O0" + using "$foptimisation" + ;; + dmd) fdebug="-debug";; + esac + cflags="$fdebugsymbols $fdebug" + unset fdebug + unset fdebugsymbols + unset foptimisation + fi +} + +## flags used in the linking step +gen_LDFLAGS () { + if [ "$dc" = ldc2 ]; then + if present ld.lld; then + ldflags="-linker=lld" + using "$ldflags" + elif present ld.gold; then + ldflags="-linker=gold" + using "$ldflags" + fi + fi +} + +# command line interface + +while getopts c:dhr ch; do + case "$ch" in + c) + case "$OPTARG" in + ldc2) dc="ldc2" ;; + dmd) dc="dmd" ;; + *) error "unknown D compiler '$OPTARG' specified (valid options: ldc2, dmd)" ;; + esac + ;; + d) debug=1 ;; + r) unset debug ;; + h) + cat <>"$mkf" +printf ' +_DC = %s +_CFLAGS = %s +_LDFLAGS = %s +' \ + "$dc" \ + "$cflags $u_cflags" \ + "$ldflags" \ + >>"$mkf" +## generate obj list +printf '_OBJS =' >>"$mkf" +for obj in $objs; do + printf " $obj" >>"$mkf" +done +printf '\n' >>"$mkf" +printf '# end generated definitions\n' >>"$mkf" + +printf "$makefile" >>"$mkf" + +## generate dependency list +>&2 printf "generating dependency list\n" +printf '\n# begin generated dependencies\n' >>"$mkf" +i=1 +for obj in $objs; do + "$dc" $u_cflags -O0 -o- -makedeps \ + "$(printf "$srcs" | awk '{print $'"$i"'}')" >>"$mkf" + i="$(($i + 1))" +done +printf '# end generated dependencies\n' >>"$mkf" diff --git a/esvapi.d b/esvapi.d index 84d17a7..f0cac7d 100644 --- a/esvapi.d +++ b/esvapi.d @@ -25,9 +25,9 @@ import std.exception : basicExceptionCtors, enforce; import std.file : tempDir, write; import std.format : format; import std.json : JSONValue, parseJSON; -import std.regex : regex, matchAll, replaceAll; +import std.regex : regex, matchAll; import std.stdio : File; -import std.string : capitalize; +import std.string : capitalize, tr; import std.net.curl : HTTP; enum ESVIndent @@ -162,8 +162,6 @@ bool verseValid(in char[] verse) @safe return false; } -} - class ESVApi { protected { @@ -234,32 +232,51 @@ class ESVApi params = []; { - (char[])[] parambuf; + void *o; + string[] parambuf; foreach (string opt; ESVAPI_PARAMETERS) { - bool bo; - int io; - switch (opt) { case "indent-using": - o = opts.indent_using; + o = cast(void *)opts.indent_using; break; case "indent-poetry": + case "include-passage-references": + case "include-verse-numbers": + case "include-first-verse-numbers": + case "include-footnotes": + case "include-footnote-body": + case "include-headings": + case "include-short-copyright": + case "include-copyright": + case "include-passage-horizontal-lines": + case "include-heading-horizontal-lines": + case "include-selahs": + o = cast(void *)opts.b[opt]; + break; + case "line-length": + case "horizontal-line-length": + case "indent-paragraphs": + case "indent-poetry-lines": + case "indent-declares": + case "indent-psalm-doxology": + o = cast(void *)opts.i[opt]; + break; + default: break; } - !opt.matchAll("^include-").empty) { - o = opts.b[opt]; - } else if (opt == "line-length" || - opt == "horizontal-line-length" || - !opt.matchAll("^indent-").empty) { - o = opts.i[opt]; - } - params = format!"%s&%s=%s"(params, opt, o.to!string()); + parambuf[parambuf.length] = format!"&%s=%s"( + opt, + opt == "indent-using" ? + opts.indent_using == ESVIndent.TAB ? "tab" : "space" + : o.to!string() + ); } } - request = HTTP( - format!"%s/text/?q=%s+%s%s%s"(_url, book + request = HTTP(format!"%s/text/?q=%s+%s%s%s"( + _url, + book .capitalize() - .replaceAll(regex(" "), "+"), + .tr(" ", "+"), verse, params, extraParameters) ); request.onProgress = onProgress; @@ -290,7 +307,12 @@ class ESVApi File tmpFile; auto request = HTTP(format!"%s/audio/?q=%s+%s"( - _url, book.capitalize().replaceAll(regex(" "), "+"), verse)); + _url, + book + .capitalize() + .tr(" ", "+"), + verse) + ); request.onProgress = onProgress; request.onReceive = (ubyte[] data) @@ -320,7 +342,8 @@ class ESVApi JSONValue json; request = HTTP(format!"%s/search/?q=%s"( - _url, query.replaceAll(regex(" "), "+"))); + _url, query.tr(" ", "+")) + ); request.onProgress = onProgress; request.onReceive = (ubyte[] data) From c21ab033eeb06e9f11e3b8583f0bce99c5fd3d12 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Sun, 24 Sep 2023 16:07:29 +1300 Subject: [PATCH 024/133] Add module statement to esv.d --- esv.d | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esv.d b/esv.d index 56686d1..9026d83 100644 --- a/esv.d +++ b/esv.d @@ -18,6 +18,8 @@ * along with esv. If not, see . */ +module esv; + import std.conv : to, ConvException; import std.exception : enforce; import std.file : exists, mkdirRecurse, write, FileException; From ca36b7fb9004981f376db4f3d01c7e1e6544f08e Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Sun, 24 Sep 2023 16:11:20 +1300 Subject: [PATCH 025/133] OpenBSD code style --- esv.d | 9 ++++++--- esvapi.d | 30 ++++++++++++++++++++---------- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/esv.d b/esv.d index 9026d83..e8dd861 100644 --- a/esv.d +++ b/esv.d @@ -206,7 +206,8 @@ key = %s esv.extraParameters = iniData["api"].getKey("parameters"); - string returnValid(string def, string val) + string + returnValid(string def, string val) { return val == "" ? def : val; } @@ -275,12 +276,14 @@ key = %s return true; } -private string extractOpt(in GetOptException e) @safe +private string +extractOpt(in GetOptException e) @safe { return e.msg.matchFirst("-.")[0]; } -private string parseBook(in string book) @safe +private string +parseBook(in string book) @safe { return book.replaceAll(regex("[-_]"), " "); } diff --git a/esvapi.d b/esvapi.d index f0cac7d..ec14723 100644 --- a/esvapi.d +++ b/esvapi.d @@ -135,7 +135,8 @@ const string[] ESVAPI_PARAMETERS = [ * Returns true if the argument book is a valid book of the Bible. * Otherwise, returns false. */ -bool bookValid(in char[] book) nothrow @safe +bool +bookValid(in char[] book) nothrow @safe { foreach (string b; BIBLE_BOOKS) { if (book.capitalize() == b.capitalize()) @@ -147,9 +148,11 @@ bool bookValid(in char[] book) nothrow @safe * Returns true if the argument book is a valid verse format. * Otherwise, returns false. */ -bool verseValid(in char[] verse) @safe +bool +verseValid(in char[] verse) @safe { - bool vMatch(in string re) @safe + bool + vMatch(in string re) @safe { return !verse.matchAll(regex(re)).empty; } @@ -188,28 +191,32 @@ class ESVApi * Returns the API authentication key that was given when the object * was constructed. This authentication key cannot be changed. */ - @property string key() const nothrow pure @safe + @property string + key() const nothrow pure @safe { return _key; } /* * Returns the subdirectory used to store temporary audio passages. */ - @property string tmpDir() const nothrow pure @safe + @property string + tmpDir() const nothrow pure @safe { return _tmp; } /* * Returns the API URL currently in use. */ - @property string url() const nothrow pure @safe + @property string + url() const nothrow pure @safe { return _url; } /* * Sets the API URL currently in use to the given url argument. */ - @property void url(immutable(string) url) @safe + @property void + url(immutable(string) url) @safe in (!url.matchAll(`^https?://.+\\..+(/.+)?`).empty, "Invalid URL format") { _url = url; @@ -222,7 +229,8 @@ class ESVApi * * Example: getVerses("John", "3:16-21") */ - string getVerses(in char[] book, in char[] verse) const + string + getVerses(in char[] book, in char[] verse) const in (bookValid(book), "Invalid book") in (verseValid(verse), "Invalid verse format") { @@ -299,7 +307,8 @@ class ESVApi * * Example: getAudioVerses("John", "3:16-21") */ - string getAudioVerses(in char[] book, in char[] verse) const + string + getAudioVerses(in char[] book, in char[] verse) const in (bookValid(book), "Invalid book") in (verseValid(verse), "Invalid verse format") { @@ -333,7 +342,8 @@ class ESVApi * * Example: search("It is finished") */ - char[] search(in string query, in bool raw = true) + char[] + search(in string query, in bool raw = true) { ulong i; char[] response; From 6e71909c6e0c6fa18c3cd0754c551f122b323d7c Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Fri, 29 Sep 2023 10:08:47 +1300 Subject: [PATCH 026/133] Improve configure script --- configure | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/configure b/configure index 8d03e11..ae27905 100755 --- a/configure +++ b/configure @@ -66,13 +66,13 @@ gen_DC () { fi if present ldc2; then dc=ldc2 - using ldc2 elif present dmd; then dc=dmd - using dmd else error "D compiler not found; install ldc or dmd" fi + + using "$dc" } ## flags used in the compilation step @@ -82,24 +82,17 @@ gen_CFLAGS () { ldc2) cflags="-Oz";; dmd) cflags="-O";; esac - using "$cflags" else - fdebugsymbols="-g" - using "$fdebugsymbols" + cflags="-g" case "$dc" in - ldc2) - fdebug="-d-debug" - using "$fdebug" - foptimisation="-O0" - using "$foptimisation" - ;; - dmd) fdebug="-debug";; + ldc2) cflags="$cflags -O0 -d-debug";; + dmd) cflags="$cflags -debug";; esac - cflags="$fdebugsymbols $fdebug" - unset fdebug - unset fdebugsymbols - unset foptimisation fi + + for flag in $cflags; do + using "$flag" + done } ## flags used in the linking step @@ -107,12 +100,18 @@ gen_LDFLAGS () { if [ "$dc" = ldc2 ]; then if present ld.lld; then ldflags="-linker=lld" - using "$ldflags" elif present ld.gold; then ldflags="-linker=gold" - using "$ldflags" fi fi + if [ -z "$debug" ]; then + if ! [ -z "$ldflags" ]; then ldflags="$ldflags "; fi + ldflags="$ldflags-L--gc-sections" + fi + + for flag in $ldflags; do + using "$flag" + done } # command line interface From 12d5d9dcea30802a75f50193d27b2865fe07416e Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Fri, 29 Sep 2023 10:09:37 +1300 Subject: [PATCH 027/133] Add search functionality and make other improvements --- esv.d | 84 +++++++++++++---------- esvapi.d | 204 ++++++++++++++++++++++++++----------------------------- 2 files changed, 148 insertions(+), 140 deletions(-) diff --git a/esv.d b/esv.d index e8dd861..8fa95d7 100644 --- a/esv.d +++ b/esv.d @@ -46,6 +46,7 @@ int lFlag; /* line length */ bool nFlag, NFlag; /* verse numbers */ bool PFlag; /* disable pager */ bool rFlag, RFlag; /* passage references */ +string sFlag; /* search passages */ bool VFlag; /* show version */ int @@ -89,21 +90,18 @@ run(string[] args) /* Parse command-line options */ try { - args.getopt( + getopt(args, getoptConfig.bundling, getoptConfig.caseSensitive, "a", &aFlag, "C", &CFlag, - "F", &FFlag, - "f", &fFlag, - "H", &HFlag, - "h", &hFlag, + "F", &FFlag, "f", &fFlag, + "H", &HFlag, "h", &hFlag, "l", &lFlag, - "N", &NFlag, - "n", &nFlag, + "N", &NFlag, "n", &nFlag, "P", &PFlag, - "R", &RFlag, - "r", &rFlag, + "R", &RFlag, "r", &rFlag, + "s", &sFlag, "V", &VFlag, ); } catch (GetOptException e) { @@ -115,7 +113,7 @@ run(string[] args) throw new Exception(e.msg); /* catch-all */ } catch (ConvException e) { throw new Exception( - "illegal argument to -l option -- integer required"); + "illegal argument to -l option -- must be integer"); } if (VFlag) { @@ -123,8 +121,13 @@ run(string[] args) return true; } + if (sFlag != "") { + /* skip argument validation */ + goto config; + } + if (args.length < 3) { - stderr.writefln("usage: %s [-C config] [-l length] [-aFfHhNnPRrV] book verses", args[0].baseName()); + stderr.writefln("usage: %s [-aFfHhNnPRrV] [-C config] [-l length] [-s query] book verses", args[0].baseName()); return false; } @@ -136,6 +139,7 @@ run(string[] args) /* determine configuration file * Options have first priority, then environment variables, * then the default path */ +config: configPath = environment.get(ENV_CONFIG, DEFAULT_CONFIGPATH) .expandTilde(); try { @@ -162,8 +166,8 @@ key = %s #[passage] #footnotes = false #headings = false -#passage_references = false -#verse_numbers = false +#passage-references = false +#verse-numbers = false "(DEFAULT_APIKEY)); } } @@ -182,16 +186,21 @@ key = %s esv = new ESVApi(apiKey); + enforce(!(aFlag && sFlag), "cannot specify both -a and -s options"); if (aFlag) { string tmpf, mpegPlayer; - /* check for mpg123 */ - enforce(executeShell( - format!"command -v %s >/dev/null 2>&1"(DEFAULT_MPEGPLAYER)).status == 0, - DEFAULT_MPEGPLAYER ~ " is required for audio mode; cannot continue"); - - tmpf = esv.getAudioVerses(args[1], args[2]); + tmpf = esv.getAudioPassage(args[1], args[2]); mpegPlayer = environment.get(ENV_PLAYER, DEFAULT_MPEGPLAYER); + + /* check for an audio player */ + enforce( + executeShell( + format!"command -v %s >/dev/null 2>&1"(mpegPlayer) + ).status == 0, + format!"%s is required for audio mode; cannot continue"(mpegPlayer) + ); + /* esv has built-in support for mpg123 and mpv. * other players will work, just recompile with * the DEFAULT_MPEGPLAYER enum set differently @@ -203,6 +212,10 @@ key = %s executeShell(mpegPlayer ~ tmpf); return true; } + if (sFlag) { + writeln(esv.searchFormat(sFlag)); + return true; + } esv.extraParameters = iniData["api"].getKey("parameters"); @@ -213,9 +226,9 @@ key = %s } /* Get [passage] keys */ - foreach (string key; ["footnotes", "headings", "passage_references", "verse_numbers"]) { + foreach (string key; ["footnotes", "headings", "passage-references", "verse-numbers"]) { try { - esv.opts.b["include_" ~ key] = + esv.opts.b["include-" ~ key] = returnValid("true", iniData["passage"].getKey(key)).to!bool(); } catch (ConvException e) { throw new Exception(format! @@ -224,27 +237,30 @@ key = %s ); } catch (IniException e) {} // just do nothing; use the default settings } - /* Get line_length ([passage]) */ - try esv.opts.i["line_length"] = returnValid("0", iniData["passage"].getKey("line_length")).to!int(); + /* Get line-length ([passage]) */ + try { + esv.opts.i["line-length"] = + returnValid("0", iniData["passage"].getKey("line-length")).to!int(); + } catch (ConvException e) { throw new Exception( format!"%s: illegal value '%s' -- must be an integer"( configPath, - iniData["passage"].getKey("line_length")) + iniData["passage"].getKey("line-length")) ); } catch (IniException e) {} // just do nothing; use the default setting - 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; - if (lFlag != 0) esv.opts.i["line_length"] = lFlag; + 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; + if (lFlag != 0) esv.opts.i["line-length"] = lFlag; - verses = esv.getVerses(args[1].parseBook(), args[2]); + verses = esv.getPassage(args[1].parseBook(), args[2]); foreach (string line; verses.splitLines()) ++lines; diff --git a/esvapi.d b/esvapi.d index ec14723..e6ca771 100644 --- a/esvapi.d +++ b/esvapi.d @@ -27,7 +27,7 @@ import std.format : format; import std.json : JSONValue, parseJSON; import std.regex : regex, matchAll; import std.stdio : File; -import std.string : capitalize, tr; +import std.string : capitalize, tr, wrap; import std.net.curl : HTTP; enum ESVIndent @@ -184,7 +184,11 @@ class ESVApi _url = ESVAPI_URL; opts = ESVApiOptions(true); extraParameters = ""; - onProgress = (size_t dlTotal, size_t dlNow, size_t ulTotal, size_t ulNow) { return 0; }; + onProgress = (size_t dlTotal, size_t dlNow, + size_t ulTotal, size_t ulNow) + { + return 0; + }; } /* @@ -222,15 +226,15 @@ class ESVApi _url = url; } /* - * Requests the verse(s) in text format from the API and returns it. + * Requests the passage in text format from the API and returns it. * The (case-insensitive) name of the book being searched are * contained in the argument book. The verse(s) being looked up are * contained in the argument verses. * - * Example: getVerses("John", "3:16-21") + * Example: getPassage("John", "3:16-21") */ string - getVerses(in char[] book, in char[] verse) const + getPassage(in char[] book, in char[] verse) in (bookValid(book), "Invalid book") in (verseValid(verse), "Invalid verse format") { @@ -242,60 +246,37 @@ class ESVApi { void *o; string[] parambuf; - foreach (string opt; ESVAPI_PARAMETERS) { - switch (opt) { - case "indent-using": - o = cast(void *)opts.indent_using; - break; - case "indent-poetry": - case "include-passage-references": - case "include-verse-numbers": - case "include-first-verse-numbers": - case "include-footnotes": - case "include-footnote-body": - case "include-headings": - case "include-short-copyright": - case "include-copyright": - case "include-passage-horizontal-lines": - case "include-heading-horizontal-lines": - case "include-selahs": - o = cast(void *)opts.b[opt]; - break; - case "line-length": - case "horizontal-line-length": - case "indent-paragraphs": - case "indent-poetry-lines": - case "indent-declares": - case "indent-psalm-doxology": - o = cast(void *)opts.i[opt]; - break; - default: break; - } - parambuf[parambuf.length] = format!"&%s=%s"( - opt, - opt == "indent-using" ? - opts.indent_using == ESVIndent.TAB ? "tab" : "space" - : o.to!string() - ); + + void + addParams(R)(R item) + { + parambuf[parambuf.length] = + format!"&%s=%s"(item.key, item.value); + } + + /* integers booleans indent_using */ + parambuf = new string[opts.i.length + opts.b.length + 1]; + + foreach (item; opts.i.byKeyValue()) + addParams(item); + foreach (item; opts.b.byKeyValue()) + addParams(item); + + parambuf[parambuf.length] = + format!"&indent-using=%s"( + opts.indent_using == ESVIndent.TAB ? "tab" : "space"); + + /* assemble string from string buffer */ + foreach (string param; parambuf) { + params ~= param; } } - request = HTTP(format!"%s/text/?q=%s+%s%s%s"( - _url, + response = makeRequest(format!"text/?q=%s+%s"( book .capitalize() .tr(" ", "+"), - verse, params, extraParameters) - ); - request.onProgress = onProgress; - request.onReceive = - (ubyte[] data) - { - response ~= data; - return data.length; - }; - request.addRequestHeader("Authorization", "Token " ~ _key); - request.perform(); + verse) ~ params ~ extraParameters); return response.parseJSON()["passages"][0].str; } /* @@ -305,23 +286,81 @@ class ESVApi * contained in the argument book. The verse(s) being looked up are * contained in the argument verses. * - * Example: getAudioVerses("John", "3:16-21") + * Example: getAudioPassage("John", "3:16-21") */ string - getAudioVerses(in char[] book, in char[] verse) const + getAudioPassage(in char[] book, in char[] verse) in (bookValid(book), "Invalid book") in (verseValid(verse), "Invalid verse format") { char[] response; - File tmpFile; + File tmpFile; + HTTP request; - auto request = HTTP(format!"%s/audio/?q=%s+%s"( - _url, + response = makeRequest(format!"audio/?q=%s+%s"( book .capitalize() .tr(" ", "+"), verse) ); + tmpFile = File(_tmp, "w"); + tmpFile.write(response); + return _tmp; + } + /* + * Requests a passage search for the given query. + * Returns a string containing JSON data representing + * the results of the search. + * + * Example: search("It is finished") + */ + char[] + search(in string query) + { + char[] response; + HTTP request; + JSONValue json; + + response = makeRequest("search/?q=" ~ query.tr(" ", "+")); + return response; + } + /* + * Calls search() and formats the results nicely as plain text. + */ + char[] + searchFormat(alias fmt = "\033[1m%s\033[0m\n %s\n") + (in string query, int lineLength = 0) /* 0 means default */ + { + JSONValue resp; + char[] layout; + + resp = parseJSON(search(query)); + layout = []; + + enforce!ESVException(resp["total_results"].integer != 0, + "No results for search"); + + lineLength = lineLength == 0 ? 80 : lineLength; + + foreach (JSONValue item; resp["results"].array) { + layout ~= format!fmt( + item["reference"].str, + item["content"].str + .wrap(lineLength) + ); + } + + return layout; + } + + protected char[] + makeRequest(in char[] query) + { + char[] response; + HTTP request; + + response = []; + request = HTTP(_url ~ "/" ~ query); request.onProgress = onProgress; request.onReceive = (ubyte[] data) @@ -331,55 +370,8 @@ class ESVApi }; request.addRequestHeader("Authorization", "Token " ~ _key); request.perform(); - tmpFile = File(_tmp, "w"); - tmpFile.write(response); - return _tmp; - } - /* - * Requests a passage search for the given query. - * If raw is false, formats the passage search as - * plain text, otherwise returns JSON from the API. - * - * Example: search("It is finished") - */ - char[] - search(in string query, in bool raw = true) - { - ulong i; - char[] response; - char[] layout; - HTTP request; - JSONValue json; - request = HTTP(format!"%s/search/?q=%s"( - _url, query.tr(" ", "+")) - ); - request.onProgress = onProgress; - request.onReceive = - (ubyte[] data) - { - response ~= cast(char[])data; - return data.length; - }; - request.addRequestHeader("Authorization", "Token " ~ _key); - request.perform(); - - if (raw) - return response; - - json = response.parseJSON(); - layout = cast(char[])""; - - enforce!ESVException(json["total_results"].integer == 0, "No results for search"); - - foreach (ulong i, JSONValue result; json["results"]) { - layout ~= format!"%s\n %s\n"( - result["reference"].str, - result["content"].str - ); - } - - return layout; + return response; } } From 60023c259e8a1962a5afd92bf16dab932da93b39 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Tue, 28 Nov 2023 10:46:35 +1300 Subject: [PATCH 028/133] Add editorconfig --- .editorconfig | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..56ad991 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +root = true + +[*] +end_of_line = lf +indent_style = space +indent_size = 4 \ No newline at end of file From 472261e15adaaebefc7f4816c998a3ef4cae0e5d Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Tue, 28 Nov 2023 10:46:59 +1300 Subject: [PATCH 029/133] Document everything and fix out-of-bounds memory error --- esv.d | 1 - esvapi.d | 121 ++++++++++++++++++++++++++++++++++--------------------- 2 files changed, 76 insertions(+), 46 deletions(-) diff --git a/esv.d b/esv.d index 8fa95d7..e2313c1 100644 --- a/esv.d +++ b/esv.d @@ -82,7 +82,6 @@ run(string[] args) ushort lines; string apiKey; string configPath; - string configEnvVar; string pager; string verses; Ini iniData; diff --git a/esvapi.d b/esvapi.d index e6ca771..6c2734b 100644 --- a/esvapi.d +++ b/esvapi.d @@ -30,13 +30,17 @@ import std.stdio : File; import std.string : capitalize, tr, wrap; import std.net.curl : HTTP; +/** Indentation style to use when formatting passages. */ enum ESVIndent { SPACE, TAB } -const enum string ESVAPI_URL = "https://api.esv.org/v3/passage"; +/** Default URL to use when sending API requests. */ +enum string ESVAPI_URL = "https://api.esv.org/v3/passage"; + +/** Constant array of all books in the Bible. */ const string[] BIBLE_BOOKS = [ /* Old Testament */ "Genesis", @@ -108,7 +112,8 @@ const string[] BIBLE_BOOKS = [ "Jude", "Revelation" ]; -/* All boolean API parameters */ + +/** All allowed API parameters (for text passages). */ const string[] ESVAPI_PARAMETERS = [ "include-passage-references", "include-verse-numbers", @@ -131,7 +136,7 @@ const string[] ESVAPI_PARAMETERS = [ "indent-using", ]; -/* +/** * Returns true if the argument book is a valid book of the Bible. * Otherwise, returns false. */ @@ -144,27 +149,32 @@ bookValid(in char[] book) nothrow @safe } return false; } -/* - * Returns true if the argument book is a valid verse format. +/** + * Returns true if the argument verse is a valid verse format. * Otherwise, returns false. */ bool verseValid(in char[] verse) @safe { - bool - vMatch(in string re) @safe - { - return !verse.matchAll(regex(re)).empty; + foreach (string re; [ + "^\\d{1,3}$", + "^\\d{1,3}-\\d{1,3}$", + "^\\d{1,3}:\\d{1,3}$", + "^\\d{1,3}:\\d{1,3}-\\d{1,3}$" + ]) { + if (!verse.matchAll(regex(re)).empty) + return true; } - if (vMatch("^\\d{1,3}$") || - vMatch("^\\d{1,3}-\\d{1,3}$") || - vMatch("^\\d{1,3}:\\d{1,3}$") || - vMatch("^\\d{1,3}:\\d{1,3}-\\d{1,3}$")) - return true; return false; } +/** + * ESV API object containing the authentication key, + * the API URL, any parameters to use when contacting the + * API as well as the temporary directory to use when + * fetching audio passages. + */ class ESVApi { protected { @@ -173,10 +183,25 @@ class ESVApi string _url; } - string extraParameters; - int delegate(size_t, size_t, size_t, size_t) onProgress; + /** + * Structure that contains associative arrays for each of + * the possible parameter types. Specify API parameters here. + */ ESVApiOptions opts; + /** + * Any additional parameters to append to the request. + * Must start with an ampersand ('&'). + */ + string extraParameters; + /** + * Callback function that is called whenever progress is made + * on a request. + */ + int delegate(size_t, size_t, size_t, size_t) onProgress; + /** + * Constructs an ESVApi object using the given authentication key. + */ this(string key, string tmpName = "esv") { _key = key; @@ -184,14 +209,14 @@ class ESVApi _url = ESVAPI_URL; opts = ESVApiOptions(true); extraParameters = ""; - onProgress = (size_t dlTotal, size_t dlNow, - size_t ulTotal, size_t ulNow) - { + onProgress = delegate int (size_t dlTotal, size_t dlNow, + size_t ulTotal, size_t ulNow) + { return 0; }; } - /* + /** * Returns the API authentication key that was given when the object * was constructed. This authentication key cannot be changed. */ @@ -200,7 +225,7 @@ class ESVApi { return _key; } - /* + /** * Returns the subdirectory used to store temporary audio passages. */ @property string @@ -208,7 +233,7 @@ class ESVApi { return _tmp; } - /* + /** * Returns the API URL currently in use. */ @property string @@ -216,7 +241,7 @@ class ESVApi { return _url; } - /* + /** * Sets the API URL currently in use to the given url argument. */ @property void @@ -225,7 +250,7 @@ class ESVApi { _url = url; } - /* + /** * Requests the passage in text format from the API and returns it. * The (case-insensitive) name of the book being searched are * contained in the argument book. The verse(s) being looked up are @@ -239,18 +264,17 @@ class ESVApi in (verseValid(verse), "Invalid verse format") { char[] params, response; - HTTP request; params = []; { - void *o; string[] parambuf; void addParams(R)(R item) { - parambuf[parambuf.length] = + parambuf.length++; + parambuf[parambuf.length - 1] = format!"&%s=%s"(item.key, item.value); } @@ -262,7 +286,7 @@ class ESVApi foreach (item; opts.b.byKeyValue()) addParams(item); - parambuf[parambuf.length] = + parambuf[parambuf.length - 1] = format!"&indent-using=%s"( opts.indent_using == ESVIndent.TAB ? "tab" : "space"); @@ -279,7 +303,7 @@ class ESVApi verse) ~ params ~ extraParameters); return response.parseJSON()["passages"][0].str; } - /* + /** * Requests an audio track of the verse(s) from the API and * returns a file path containing an MP3 sound track. * The (case-insensitive) name of the book being searched are @@ -293,21 +317,18 @@ class ESVApi in (bookValid(book), "Invalid book") in (verseValid(verse), "Invalid verse format") { - char[] response; File tmpFile; - HTTP request; - response = makeRequest(format!"audio/?q=%s+%s"( + tmpFile = File(_tmp, "w"); + tmpFile.write(makeRequest(format!"audio/?q=%s+%s"( book .capitalize() .tr(" ", "+"), verse) - ); - tmpFile = File(_tmp, "w"); - tmpFile.write(response); + )); return _tmp; } - /* + /** * Requests a passage search for the given query. * Returns a string containing JSON data representing * the results of the search. @@ -317,22 +338,17 @@ class ESVApi char[] search(in string query) { - char[] response; - HTTP request; - JSONValue json; - - response = makeRequest("search/?q=" ~ query.tr(" ", "+")); - return response; + return makeRequest("search/?q=" ~ query.tr(" ", "+")); } - /* + /** * Calls search() and formats the results nicely as plain text. */ char[] searchFormat(alias fmt = "\033[1m%s\033[0m\n %s\n") - (in string query, int lineLength = 0) /* 0 means default */ + (in string query, int lineLength = 0) /* 0 means default */ { - JSONValue resp; char[] layout; + JSONValue resp; resp = parseJSON(search(query)); layout = []; @@ -375,12 +391,22 @@ class ESVApi } } +/** + * Structure containing parameters passed to the ESV API. + */ struct ESVApiOptions { + /** Boolean options */ bool[string] b; + /** Integer options */ int[string] i; + /** Indentation style to use when formatting text passages. */ ESVIndent indent_using; + /** + * If initialise is true, initialise an ESVApiOptions + * structure with the default values. + */ this(bool initialise) nothrow @safe { if (!initialise) @@ -408,6 +434,11 @@ struct ESVApiOptions } } +/** + * Exception thrown on API errors. + * Currently only used when there is no search results + * following a call of searchFormat. + */ class ESVException : Exception { mixin basicExceptionCtors; From 97c9c427ff5be16f8bdaefe5365291b74bbcaf24 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Tue, 5 Dec 2023 08:59:35 +1300 Subject: [PATCH 030/133] Add @safe and final where possible, remove unnecessary documentation and fix incorrect indentation --- esv.d | 2 +- esvapi.d | 53 +++++++++++++++++++++-------------------------------- 2 files changed, 22 insertions(+), 33 deletions(-) diff --git a/esv.d b/esv.d index e2313c1..dad6f66 100644 --- a/esv.d +++ b/esv.d @@ -219,7 +219,7 @@ key = %s esv.extraParameters = iniData["api"].getKey("parameters"); string - returnValid(string def, string val) + returnValid(string def, string val) @safe { return val == "" ? def : val; } diff --git a/esvapi.d b/esvapi.d index 6c2734b..e436884 100644 --- a/esvapi.d +++ b/esvapi.d @@ -162,8 +162,8 @@ verseValid(in char[] verse) @safe "^\\d{1,3}:\\d{1,3}$", "^\\d{1,3}:\\d{1,3}-\\d{1,3}$" ]) { - if (!verse.matchAll(regex(re)).empty) - return true; + if (!verse.matchAll(regex(re)).empty) + return true; } return false; @@ -183,26 +183,17 @@ class ESVApi string _url; } - /** - * Structure that contains associative arrays for each of - * the possible parameter types. Specify API parameters here. - */ ESVApiOptions opts; - /** - * Any additional parameters to append to the request. - * Must start with an ampersand ('&'). - */ + + /** Additional request parameters */ string extraParameters; - /** - * Callback function that is called whenever progress is made - * on a request. - */ + /** Called whenever progress is made on a request. */ int delegate(size_t, size_t, size_t, size_t) onProgress; - /** - * Constructs an ESVApi object using the given authentication key. - */ - this(string key, string tmpName = "esv") + /** + * Constructs an ESVApi object using the given authentication key. + */ + this(string key, string tmpName = "esv") @safe { _key = key; _tmp = tempDir() ~ tmpName; @@ -220,31 +211,31 @@ class ESVApi * Returns the API authentication key that was given when the object * was constructed. This authentication key cannot be changed. */ - @property string - key() const nothrow pure @safe + final @property string + key() const nothrow pure @nogc @safe { return _key; } /** * Returns the subdirectory used to store temporary audio passages. */ - @property string - tmpDir() const nothrow pure @safe + final @property string + tmpDir() const nothrow pure @nogc @safe { return _tmp; } /** * Returns the API URL currently in use. */ - @property string - url() const nothrow pure @safe + final @property string + url() const nothrow pure @nogc @safe { return _url; } /** * Sets the API URL currently in use to the given url argument. */ - @property void + final @property void url(immutable(string) url) @safe in (!url.matchAll(`^https?://.+\\..+(/.+)?`).empty, "Invalid URL format") { @@ -396,17 +387,14 @@ class ESVApi */ struct ESVApiOptions { - /** Boolean options */ bool[string] b; - /** Integer options */ int[string] i; - /** Indentation style to use when formatting text passages. */ ESVIndent indent_using; - /** - * If initialise is true, initialise an ESVApiOptions - * structure with the default values. - */ + /** + * If initialise is true, initialise an ESVApiOptions + * structure with the default values. + */ this(bool initialise) nothrow @safe { if (!initialise) @@ -436,6 +424,7 @@ struct ESVApiOptions /** * Exception thrown on API errors. + * * Currently only used when there is no search results * following a call of searchFormat. */ From c5c9930a5584e593584ece1fb52e7f0a4d56d79f Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Wed, 6 Dec 2023 13:51:08 +1300 Subject: [PATCH 031/133] Fix weird indentation bug ...by setting the default indentation character to a space. --- esvapi.d | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/esvapi.d b/esvapi.d index e436884..4c49ee1 100644 --- a/esvapi.d +++ b/esvapi.d @@ -269,7 +269,6 @@ class ESVApi format!"&%s=%s"(item.key, item.value); } - /* integers booleans indent_using */ parambuf = new string[opts.i.length + opts.b.length + 1]; foreach (item; opts.i.byKeyValue()) @@ -418,7 +417,7 @@ struct ESVApiOptions i["indent-declares"] = 40; i["indent-psalm-doxology"] = 30; i["line-length"] = 0; - indent_using = ESVIndent.TAB; + indent_using = ESVIndent.SPACE; } } From 775dc924a5e330ba8a923b2c124edf81a965cc14 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Wed, 6 Dec 2023 15:59:35 +1300 Subject: [PATCH 032/133] Add bounds check to build and fix editorconfig --- .editorconfig | 2 +- configure | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.editorconfig b/.editorconfig index 56ad991..5ad5271 100644 --- a/.editorconfig +++ b/.editorconfig @@ -2,5 +2,5 @@ root = true [*] end_of_line = lf -indent_style = space +indent_style = tab indent_size = 4 \ No newline at end of file diff --git a/configure b/configure index ae27905..399c603 100755 --- a/configure +++ b/configure @@ -4,14 +4,14 @@ set -e -IMPORT=import +import=import mkf=Makefile -cflags=-I"$IMPORT" +cflags='-I'"$import"' -boundscheck=on' objs='esv.o esvapi.o' srcs='esv.d esvapi.d' makefile=' -IMPORT = '"$IMPORT"' +IMPORT = '"$import"' PREFIX = /usr/local MANPREFIX = ${PREFIX}/man From 35a880ae264ae40ffbfa66501cfad1de2b6438e4 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Thu, 14 Dec 2023 09:45:48 +1300 Subject: [PATCH 033/133] Fix build on OpenBSD --- configure | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/configure b/configure index 399c603..481c82a 100755 --- a/configure +++ b/configure @@ -7,7 +7,7 @@ set -e import=import mkf=Makefile -cflags='-I'"$import"' -boundscheck=on' +cflags='-I'"$import" objs='esv.o esvapi.o' srcs='esv.d esvapi.d' makefile=' @@ -17,7 +17,9 @@ MANPREFIX = ${PREFIX}/man DC = ${_DC} CFLAGS = ${_CFLAGS} -OBJS = ${_OBJS} ini.o +OBJS = ${_OBJS} ini.a +INIOBJS = ${IMPORT}/dini/package.o ${IMPORT}/dini/parser.o \\ + ${IMPORT}/dini/reader.o ${IMPORT}/dini/utils.o all: esv @@ -29,11 +31,15 @@ esv: ${OBJS} .d.o: ${DC} ${CFLAGS} -c $< -ini.o: ${IMPORT}/dini/*.d - ${DC} ${CFLAGS} -of=ini.o -c ${IMPORT}/dini/*.d +ini.a: ${IMPORT}/dini/{package,parser,reader,utils}.d + ${DC} ${CFLAGS} -of=${IMPORT}/dini/package.o -c ${IMPORT}/dini/package.d + ${DC} ${CFLAGS} -of=${IMPORT}/dini/parser.o -c ${IMPORT}/dini/parser.d + ${DC} ${CFLAGS} -of=${IMPORT}/dini/reader.o -c ${IMPORT}/dini/reader.d + ${DC} ${CFLAGS} -of=${IMPORT}/dini/utils.o -c ${IMPORT}/dini/utils.d + ar -cr $@ ${INIOBJS} clean: - rm -f esv ${OBJS} + rm -f esv ${OBJS} ${INIOBJS} install: esv install -m755 esv ${DESTDIR}${PREFIX}/bin/esv From 1c9bb056e80afd7af265fa51701b54fcc48b9420 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Thu, 14 Dec 2023 10:19:33 +1300 Subject: [PATCH 034/133] Remove automatic pager functionality --- README.md | 10 ---------- config.di | 2 -- esv.1 | 16 ---------------- esv.d | 37 ++----------------------------------- 4 files changed, 2 insertions(+), 63 deletions(-) diff --git a/README.md b/README.md index d69a187..c3b8b74 100644 --- a/README.md +++ b/README.md @@ -20,11 +20,6 @@ A Psalm of David. He makes me lie down in green pastures.... ``` -If the requested passage is over 32 lines long, `esv` will pipe it through -a pager (default less). The pager being used can be changed through the -`ESV_PAGER` environment variable or just disabled altogether by passing the --P option. - The names of Bible books are not case sensitive, so John, john, and JOHN are all accepted. @@ -63,11 +58,6 @@ $ make # make install ``` - - ## Documentation All documentation is contained in the manual pages. To access them, you can run diff --git a/config.di b/config.di index e40261f..8c14aab 100644 --- a/config.di +++ b/config.di @@ -7,10 +7,8 @@ public: enum DEFAULT_APIKEY = "abfb7456fa52ec4292c79e435890cfa3df14dc2b"; enum DEFAULT_CONFIGPATH = "~/.config/esv.conf"; enum DEFAULT_MPEGPLAYER = "mpg123"; -enum DEFAULT_PAGER = "less"; enum ENV_CONFIG = "ESV_CONFIG"; -enum ENV_PAGER = "ESV_PAGER"; enum ENV_PLAYER = "ESV_PLAYER"; enum BUGREPORTURL = "https://codeberg.org/jtbx/esv/issues"; diff --git a/esv.1 b/esv.1 index 6ec92ab..3957859 100644 --- a/esv.1 +++ b/esv.1 @@ -24,14 +24,6 @@ to fast-forward, etc. Read about the .Fl C option in mpg123's manual for more information. .Pp -If a text passage is too long for standard display on a terminal, -.Nm -will put it through a text pager (default less) in order for you to be able to -scroll through the text. This behaviour can be disabled by passing -the -.Fl P -flag. -.Pp The options are as follows: .Bl -tag -width keyword .It Fl a @@ -59,9 +51,6 @@ as the maximum line length. Exclude verse numbers. .It Fl n Include verse numbers (the default). -.It Fl P -If the passage is over 32 lines long, don't -pipe it into a pager. .It Fl R Exclude passage references. .It Fl r @@ -73,11 +62,6 @@ Print the version number and exit. .It Ev ESV_CONFIG Where to read the configuration file, rather than using the default location (see section .Sx FILES ) . -.It Ev ESV_PAGER -What pager to use when the passage is over 32 lines long, rather than using -the -.Ic less -utility. .It Ev ESV_PLAYER What MP3 player to use for playing audio, rather than using mpg123. Using mpg123 is recommended over other players such as mpv, because diff --git a/esv.d b/esv.d index dad6f66..760f4f8 100644 --- a/esv.d +++ b/esv.d @@ -44,7 +44,6 @@ bool fFlag, FFlag; /* footnotes */ bool hFlag, HFlag; /* headings */ int lFlag; /* line length */ bool nFlag, NFlag; /* verse numbers */ -bool PFlag; /* disable pager */ bool rFlag, RFlag; /* passage references */ string sFlag; /* search passages */ bool VFlag; /* show version */ @@ -79,11 +78,8 @@ main(string[] args) bool run(string[] args) { - ushort lines; string apiKey; string configPath; - string pager; - string verses; Ini iniData; ESVApi esv; @@ -98,7 +94,6 @@ run(string[] args) "H", &HFlag, "h", &hFlag, "l", &lFlag, "N", &NFlag, "n", &nFlag, - "P", &PFlag, "R", &RFlag, "r", &rFlag, "s", &sFlag, "V", &VFlag, @@ -126,7 +121,7 @@ run(string[] args) } if (args.length < 3) { - stderr.writefln("usage: %s [-aFfHhNnPRrV] [-C config] [-l length] [-s query] book verses", args[0].baseName()); + stderr.writefln("usage: %s [-aFfHhNnRrV] [-C config] [-l length] [-s query] book verses", args[0].baseName()); return false; } @@ -259,35 +254,7 @@ key = %s if (RFlag) esv.opts.b["include-passage-references"] = false; if (lFlag != 0) esv.opts.i["line-length"] = lFlag; - verses = esv.getPassage(args[1].parseBook(), args[2]); - foreach (string line; verses.splitLines()) - ++lines; - - /* If the passage is very long, pipe it into a pager */ - if (lines > 32 && !PFlag) { - import std.process : pipeProcess, Redirect, wait, ProcessException; - - pager = environment.get(ENV_PAGER, DEFAULT_PAGER); - try { - auto pipe = pipeProcess(pager, Redirect.stdin); - pipe.stdin.writeln(verses); - pipe.stdin.flush(); - pipe.stdin.close(); - pipe.pid.wait(); - } catch (ProcessException e) { - enforce(e.msg.matchFirst(regex("^Executable file not found")).empty, - format!"%s: command not found"(e.msg - .matchFirst(": (.+)$")[0] - .replaceFirst(regex("^: "), "") - )); - - throw new Exception(e.msg); /* catch-all */ - } - - return true; - } - - writeln(verses); + writeln(esv.getPassage(args[1].parseBook(), args[2])); return true; } From aecbdfec4f8fa05ac299697e2b15d6c6a80e2766 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Thu, 14 Dec 2023 19:54:57 +1300 Subject: [PATCH 035/133] Add call to pledge() on OpenBSD --- esv.d | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/esv.d b/esv.d index 760f4f8..de6dc6a 100644 --- a/esv.d +++ b/esv.d @@ -48,13 +48,25 @@ bool rFlag, RFlag; /* passage references */ string sFlag; /* search passages */ bool VFlag; /* show version */ +version (OpenBSD) { + immutable(char) *promises; +} + int main(string[] args) { bool success; + version (OpenBSD) { + import core.sys.openbsd.unistd : pledge; + import std.string : toStringz; + + promises = toStringz("stdio rpath wpath cpath inet dns proc exec prot_exec"); + pledge(promises, null); + } + debug { - return run(args); + return run(args) ? 0 : 1; } try { From 9311b19f6dbd22f3af115caf19034cf896e76222 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Tue, 19 Dec 2023 17:14:30 +1300 Subject: [PATCH 036/133] update manual --- esv.1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esv.1 b/esv.1 index 3957859..ee69fc9 100644 --- a/esv.1 +++ b/esv.1 @@ -1,4 +1,4 @@ -.Dd $Mdocdate: March 23 2023 $ +.Dd $Mdocdate: December 19 2023 $ .Dt ESV 1 .Os .Sh NAME @@ -7,9 +7,9 @@ .Sh SYNOPSIS .Nm esv .Bk -words +.Op Fl aFfHhNnRrV .Op Fl C Ar config .Op Fl l Ar length -.Op Fl aFfHhNnRrV .Ar book verses .Ek .Sh DESCRIPTION From de9e18f06ea26f51b69339b20a277f8039444567 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Tue, 13 Feb 2024 09:31:04 +1300 Subject: [PATCH 037/133] update copyright year to 2024 --- esv.d | 2 +- esvapi.d | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esv.d b/esv.d index de6dc6a..0c2cf67 100644 --- a/esv.d +++ b/esv.d @@ -2,7 +2,7 @@ * esv: read the Bible from your terminal * * The GPLv2 License (GPLv2) - * Copyright (c) 2023 Jeremy Baxter + * Copyright (c) 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 diff --git a/esvapi.d b/esvapi.d index 4c49ee1..79e70f5 100644 --- a/esvapi.d +++ b/esvapi.d @@ -2,7 +2,7 @@ * esvapi.d: a reusable interface to the ESV HTTP API * * The GPLv2 License (GPLv2) - * Copyright (c) 2023 Jeremy Baxter + * Copyright (c) 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 From 2e93c891fdc9bfbb902224b430f09e3fcf2bda99 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Tue, 13 Feb 2024 09:31:56 +1300 Subject: [PATCH 038/133] migrate to initial.d over dini initial.d is my very own INI parser, see --- COPYING | 34 +- configure | 23 +- esv.d | 51 +-- import/dini/LICENSE | 23 -- import/dini/README | 30 -- import/dini/package.d | 3 - import/dini/parser.d | 681 ------------------------------------ import/dini/reader.d | 786 ------------------------------------------ import/dini/utils.d | 66 ---- initial.d | 454 ++++++++++++++++++++++++ 10 files changed, 509 insertions(+), 1642 deletions(-) delete mode 100644 import/dini/LICENSE delete mode 100644 import/dini/README delete mode 100644 import/dini/package.d delete mode 100644 import/dini/parser.d delete mode 100644 import/dini/reader.d delete mode 100644 import/dini/utils.d create mode 100644 initial.d diff --git a/COPYING b/COPYING index f93c1b2..de62dba 100644 --- a/COPYING +++ b/COPYING @@ -1,5 +1,35 @@ -This software is licensed under the GNU General Public License, version 2. -The license is as follows: +The file initial.d is licensed under the Boost Software License, +Version 1.0. The contents of that license follows: + +Copyright (c) 2024 Jeremy Baxter. All rights reserved. + +Boost Software License - Version 1.0 - August 17th, 2003 + +Permission is hereby granted, free of charge, to any person or organization +obtaining a copy of the software and accompanying documentation covered by +this license (the "Software") to use, reproduce, display, distribute, +execute, and transmit the Software, and to prepare derivative works of the +Software, and to permit third-parties to whom the Software is furnished to +do so, all subject to the following: + +The copyright notices in the Software and this entire statement, including +the above license grant, this restriction and the following disclaimer, +must be included in all copies of the Software, in whole or in part, and +all derivative works of the Software, unless such copies or derivative +works are solely in the form of machine-executable object code generated by +a source language processor. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT +SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + + +This software (esv) is licensed under the GNU General Public License, +version 2. The contents of that license follows: GNU GENERAL PUBLIC LICENSE Version 2, June 1991 diff --git a/configure b/configure index 481c82a..deb818c 100755 --- a/configure +++ b/configure @@ -7,9 +7,8 @@ set -e import=import mkf=Makefile -cflags='-I'"$import" -objs='esv.o esvapi.o' -srcs='esv.d esvapi.d' +objs='esv.o esvapi.o initial.o' +srcs='esv.d esvapi.d initial.d' makefile=' IMPORT = '"$import"' PREFIX = /usr/local @@ -17,9 +16,7 @@ MANPREFIX = ${PREFIX}/man DC = ${_DC} CFLAGS = ${_CFLAGS} -OBJS = ${_OBJS} ini.a -INIOBJS = ${IMPORT}/dini/package.o ${IMPORT}/dini/parser.o \\ - ${IMPORT}/dini/reader.o ${IMPORT}/dini/utils.o +OBJS = ${_OBJS} all: esv @@ -31,13 +28,6 @@ esv: ${OBJS} .d.o: ${DC} ${CFLAGS} -c $< -ini.a: ${IMPORT}/dini/{package,parser,reader,utils}.d - ${DC} ${CFLAGS} -of=${IMPORT}/dini/package.o -c ${IMPORT}/dini/package.d - ${DC} ${CFLAGS} -of=${IMPORT}/dini/parser.o -c ${IMPORT}/dini/parser.d - ${DC} ${CFLAGS} -of=${IMPORT}/dini/reader.o -c ${IMPORT}/dini/reader.d - ${DC} ${CFLAGS} -of=${IMPORT}/dini/utils.o -c ${IMPORT}/dini/utils.d - ar -cr $@ ${INIOBJS} - clean: rm -f esv ${OBJS} ${INIOBJS} @@ -152,9 +142,6 @@ done # creating the makefile -u_cflags="$cflags" -unset cflags - gen_DC gen_CFLAGS gen_LDFLAGS @@ -167,7 +154,7 @@ _CFLAGS = %s _LDFLAGS = %s ' \ "$dc" \ - "$cflags $u_cflags" \ + "$cflags" \ "$ldflags" \ >>"$mkf" ## generate obj list @@ -185,7 +172,7 @@ printf "$makefile" >>"$mkf" printf '\n# begin generated dependencies\n' >>"$mkf" i=1 for obj in $objs; do - "$dc" $u_cflags -O0 -o- -makedeps \ + "$dc" -O0 -o- -makedeps \ "$(printf "$srcs" | awk '{print $'"$i"'}')" >>"$mkf" i="$(($i + 1))" done diff --git a/esv.d b/esv.d index 0c2cf67..a03c586 100644 --- a/esv.d +++ b/esv.d @@ -32,9 +32,10 @@ import std.regex : regex, matchFirst, replaceAll, replaceFirst; import std.stdio : writef, writeln, writefln, stderr; import std.string : splitLines; +import initial; + import config; import esvapi; -import dini; enum VERSION = "0.2.0"; @@ -92,7 +93,7 @@ run(string[] args) { string apiKey; string configPath; - Ini iniData; + INIUnit ini; ESVApi esv; /* Parse command-line options */ @@ -177,17 +178,14 @@ key = %s "(DEFAULT_APIKEY)); } } - iniData = Ini.Parse(configPath); + readINIFile(ini, configPath); } catch (FileException e) { /* filesystem syscall errors */ throw new Exception(e.msg); } - try { - apiKey = iniData["api"].getKey("key"); - } catch (IniException e) { - apiKey = ""; - } - enforce(apiKey != "", + + apiKey = ini["api"].key("key"); + enforce(apiKey != null, "API key not present in configuration file; cannot proceed"); esv = new ESVApi(apiKey); @@ -223,38 +221,23 @@ key = %s return true; } - esv.extraParameters = iniData["api"].getKey("parameters"); - - string - returnValid(string def, string val) @safe - { - return val == "" ? def : val; - } + esv.extraParameters = ini["api"].key("parameters", ""); /* Get [passage] keys */ foreach (string key; ["footnotes", "headings", "passage-references", "verse-numbers"]) { - try { + try esv.opts.b["include-" ~ key] = - returnValid("true", iniData["passage"].getKey(key)).to!bool(); - } catch (ConvException e) { - throw new Exception(format! - "%s: key '%s' of section 'passage' is not a boolean value (must be either 'true' or 'false')" - (configPath, key) - ); - } catch (IniException e) {} // just do nothing; use the default settings + ini["passage"].keyAs!bool(key, "true"); + catch (INITypeException e) + throw new Exception(configPath ~ ": " ~ e.msg); } /* Get line-length ([passage]) */ try { esv.opts.i["line-length"] = - returnValid("0", iniData["passage"].getKey("line-length")).to!int(); + ini["passage"].keyAs!int("line-length", "0"); + } catch (INITypeException e) { + throw new Exception(configPath ~ ": " ~ e.msg); } - catch (ConvException e) { - throw new Exception( - format!"%s: illegal value '%s' -- must be an integer"( - configPath, - iniData["passage"].getKey("line-length")) - ); - } catch (IniException e) {} // just do nothing; use the default setting if (fFlag) esv.opts.b["include-footnotes"] = true; if (hFlag) esv.opts.b["include-headings"] = true; @@ -279,5 +262,7 @@ extractOpt(in GetOptException e) @safe private string parseBook(in string book) @safe { - return book.replaceAll(regex("[-_]"), " "); + import std.string : tr; + + return book.tr("-_", " "); } diff --git a/import/dini/LICENSE b/import/dini/LICENSE deleted file mode 100644 index 36b7cd9..0000000 --- a/import/dini/LICENSE +++ /dev/null @@ -1,23 +0,0 @@ -Boost Software License - Version 1.0 - August 17th, 2003 - -Permission is hereby granted, free of charge, to any person or organization -obtaining a copy of the software and accompanying documentation covered by -this license (the "Software") to use, reproduce, display, distribute, -execute, and transmit the Software, and to prepare derivative works of the -Software, and to permit third-parties to whom the Software is furnished to -do so, all subject to the following: - -The copyright notices in the Software and this entire statement, including -the above license grant, this restriction and the following disclaimer, -must be included in all copies of the Software, in whole or in part, and -all derivative works of the Software, unless such copies or derivative -works are solely in the form of machine-executable object code generated by -a source language processor. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT -SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE -FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, -ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. diff --git a/import/dini/README b/import/dini/README deleted file mode 100644 index 6d53e26..0000000 --- a/import/dini/README +++ /dev/null @@ -1,30 +0,0 @@ -This code is a modified version of dini, found here: - -My changes can be found here: - - -Licensed under the Boost Software License - Version 1.0: - -Boost Software License - Version 1.0 - August 17th, 2003 - -Permission is hereby granted, free of charge, to any person or organization -obtaining a copy of the software and accompanying documentation covered by -this license (the "Software") to use, reproduce, display, distribute, -execute, and transmit the Software, and to prepare derivative works of the -Software, and to permit third-parties to whom the Software is furnished to -do so, all subject to the following: - -The copyright notices in the Software and this entire statement, including -the above license grant, this restriction and the following disclaimer, -must be included in all copies of the Software, in whole or in part, and -all derivative works of the Software, unless such copies or derivative -works are solely in the form of machine-executable object code generated by -a source language processor. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT -SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE -FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, -ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. diff --git a/import/dini/package.d b/import/dini/package.d deleted file mode 100644 index 309d84e..0000000 --- a/import/dini/package.d +++ /dev/null @@ -1,3 +0,0 @@ -module dini; - -public import dini.parser; \ No newline at end of file diff --git a/import/dini/parser.d b/import/dini/parser.d deleted file mode 100644 index 66ba3fb..0000000 --- a/import/dini/parser.d +++ /dev/null @@ -1,681 +0,0 @@ -/** - * INI parsing functionality. - * - * Examples: - * --- - * auto ini = Ini.ParseString("test = bar") - * writeln("Value of test is ", ini["test"]); - * --- - */ -module dini.parser; - -import std.algorithm : min, max, countUntil; -import std.array : split, replaceInPlace, join; -import std.file : readText; -import std.stdio : File; -import std.string : strip, splitLines; -import std.traits : isSomeString; -import std.range : ElementType; -import std.conv : to; -import dini.reader : UniversalINIReader, INIException, INIToken; - - -/** - * Represents ini section - * - * Example: - * --- - * Ini ini = Ini.Parse("path/to/your.conf"); - * string value = ini.getKey("a"); - * --- - */ -struct IniSection -{ - /// Section name - protected string _name = "root"; - - /// Parent - /// Null if none - protected IniSection* _parent; - - /// Childs - protected IniSection[] _sections; - - /// Keys - protected string[string] _keys; - - - - /** - * Creates new IniSection instance - * - * Params: - * name = Section name - */ - public this(string name) - { - _name = name; - _parent = null; - } - - - /** - * Creates new IniSection instance - * - * Params: - * name = Section name - * parent = Section parent - */ - public this(string name, IniSection* parent) - { - _name = name; - _parent = parent; - } - - /** - * Sets section key - * - * Params: - * name = Key name - * value = Value to set - */ - public void setKey(string name, string value) - { - _keys[name] = value; - } - - /** - * Checks if specified key exists - * - * Params: - * name = Key name - * - * Returns: - * True if exists, false otherwise - */ - public bool hasKey(string name) @safe nothrow @nogc - { - return (name in _keys) !is null; - } - - /** - * Gets key value - * - * Params: - * name = Key name - * - * Returns: - * Key value - */ - public string getKey(string name) - { - if(!hasKey(name)) { - return ""; - } - - return _keys[name]; - } - - - /// ditto - alias getKey opCall; - - /** - * Gets key value or defaultValue if key does not exist - * - * Params: - * name = Key name - * defaultValue = Default value - * - * Returns: - * Key value or defaultValue - * - */ - public string getKey(string name, string defaultValue) @safe nothrow - { - return hasKey(name) ? _keys[name] : defaultValue; - } - - /** - * Removes key - * - * Params: - * name = Key name - */ - public void removeKey(string name) - { - _keys.remove(name); - } - - /** - * Adds section - * - * Params: - * section = Section to add - */ - public void addSection(ref IniSection section) - { - _sections ~= section; - } - - /** - * Checks if specified section exists - * - * Params: - * name = Section name - * - * Returns: - * True if exists, false otherwise - */ - public bool hasSection(string name) - { - foreach(ref section; _sections) - { - if(section.name() == name) - return true; - } - - return false; - } - - /** - * Returns reference to section - * - * Params: - * Section name - * - * Returns: - * Section with specified name - */ - public ref IniSection getSection(string name) - { - foreach(ref section; _sections) - { - if(section.name() == name) - return section; - } - - throw new IniException("Section '"~name~"' does not exist"); - } - - - /// ditto - public alias getSection opIndex; - - /** - * Removes section - * - * Params: - * name = Section name - */ - public void removeSection(string name) - { - IniSection[] childs; - - foreach(section; _sections) - { - if(section.name != name) - childs ~= section; - } - - _sections = childs; - } - - /** - * Section name - * - * Returns: - * Section name - */ - public string name() @property - { - return _name; - } - - /** - * Array of keys - * - * Returns: - * Associative array of keys - */ - public string[string] keys() @property - { - return _keys; - } - - /** - * Array of sections - * - * Returns: - * Array of sections - */ - public IniSection[] sections() @property - { - return _sections; - } - - /** - * Root section - */ - public IniSection root() @property - { - IniSection s = this; - - while(s.getParent() != null) - s = *(s.getParent()); - - return s; - } - - /** - * Section parent - * - * Returns: - * Pointer to parent, or null if parent does not exists - */ - public IniSection* getParent() - { - return _parent; - } - - /** - * Checks if current section has parent - * - * Returns: - * True if section has parent, false otherwise - */ - public bool hasParent() - { - return _parent != null; - } - - /** - * Moves current section to another one - * - * Params: - * New parent - */ - public void setParent(ref IniSection parent) - { - _parent.removeSection(this.name); - _parent = &parent; - parent.addSection(this); - } - - - /** - * Parses filename - * - * Params: - * filename = Configuration filename - * doLookups = Should variable lookups be resolved after parsing? - */ - public void parse(string filename, bool doLookups = true) - { - parseString(readText(filename), doLookups); - } - - public void parse(File* file, bool doLookups = true) - { - string data = file.byLine().join().to!string; - parseString(data, doLookups); - } - - public void parseWith(Reader)(string filename, bool doLookups = true) - { - parseStringWith!Reader(readText(filename), doLookups); - } - - public void parseWith(Reader)(File* file, bool doLookups = true) - { - string data = file.byLine().join().to!string; - parseStringWith!Reader(data, doLookups); - } - - public void parseString(string data, bool doLookups = true) - { - parseStringWith!UniversalINIReader(data, doLookups); - } - - public void parseStringWith(Reader)(string data, bool doLookups = true) - { - IniSection* section = &this; - - auto reader = Reader(data); - alias KeyType = reader.KeyType; - while (reader.next()) switch (reader.type) with (INIToken) { - case SECTION: - section = &this; - string name = reader.value.get!string; - auto parts = name.split(":"); - - // [section : parent] - if (parts.length > 1) - name = parts[0].strip; - - IniSection child = IniSection(name, section); - - if (parts.length > 1) { - string parent = parts[1].strip; - child.inherit(section.getSectionEx(parent)); - } - section.addSection(child); - section = §ion.getSection(name); - break; - - case KEY: - section.setKey(reader.value.get!KeyType.name, reader.value.get!KeyType.value); - break; - - default: - break; - } - - if(doLookups == true) - parseLookups(); - } - - /** - * Parses lookups - */ - public void parseLookups() - { - foreach (name, ref value; _keys) - { - ptrdiff_t start = -1; - char[] buf; - - foreach (i, c; value) { - if (c == '%') { - if (start != -1) { - IniSection sect; - string newValue; - char[][] parts; - - if (buf[0] == '.') { - parts = buf[1..$].split("."); - sect = this.root; - } - else { - parts = buf.split("."); - sect = this; - } - - newValue = sect.getSectionEx(parts[0..$-1].join(".").idup).getKey(parts[$-1].idup); - - value.replaceInPlace(start, i+1, newValue); - start = -1; - buf = []; - } - else { - start = i; - } - } - else if (start != -1) { - buf ~= c; - } - } - } - - foreach(child; _sections) { - child.parseLookups(); - } - } - - /** - * Returns section by name in inheriting(names connected by dot) - * - * Params: - * name = Section name - * - * Returns: - * Section - */ - public IniSection getSectionEx(string name) - { - IniSection* root = &this; - auto parts = name.split("."); - - foreach(part; parts) { - root = (&root.getSection(part)); - } - - return *root; - } - - /** - * Inherits keys from section - * - * Params: - * Section to inherit - */ - public void inherit(IniSection sect) - { - this._keys = sect.keys().dup; - } - - - public void save(string filename) - { - import std.file; - - if (exists(filename)) - remove(filename); - - File file = File(filename, "w"); - - foreach (section; _sections) { - file.writeln("[" ~ section.name() ~ "]"); - - string[string] propertiesInSection = section.keys(); - foreach (key; propertiesInSection.keys) { - file.writeln(key ~ " = " ~ propertiesInSection[key]); - } - - file.writeln(); - } - - file.close(); - } - - - /** - * Parses Ini file - * - * Params: - * filename = Path to ini file - * - * Returns: - * IniSection root - */ - static Ini Parse(string filename, bool parseLookups = true) - { - Ini i; - i.parse(filename, parseLookups); - return i; - } - - - /** - * Parses Ini file with specified reader - * - * Params: - * filename = Path to ini file - * - * Returns: - * IniSection root - */ - static Ini ParseWith(Reader)(string filename, bool parseLookups = true) - { - Ini i; - i.parseWith!Reader(filename, parseLookups); - return i; - } - - static Ini ParseString(string data, bool parseLookups = true) - { - Ini i; - i.parseString(data, parseLookups); - return i; - } - - static Ini ParseStringWith(Reader)(string data, bool parseLookups = true) - { - Ini i; - i.parseStringWith!Reader(data, parseLookups); - return i; - } -} - -// Compat -alias INIException IniException; - -/// ditto -alias IniSection Ini; - - -/// -Struct siphon(Struct)(Ini ini) -{ - import std.traits; - Struct ans; - if(ini.hasSection(Struct.stringof)) - foreach(ti, Name; FieldNameTuple!(Struct)) - { - alias ToType = typeof(ans.tupleof[ti]); - if(ini[Struct.stringof].hasKey(Name)) - ans.tupleof[ti] = to!ToType(ini[Struct.stringof].getKey(Name)); - } - return ans; -} - -unittest { - struct Section { - int var; - } - - auto ini = Ini.ParseString("[Section]\nvar=3"); - auto m = ini.siphon!Section; - assert(m.var == 3); -} - - -unittest { - auto data = q"( -key1 = value - -# comment - -test = bar ; comment - -[section 1] -key1 = new key -num = 151 -empty - - -[ various ] -"quoted key"= VALUE 123 - -quote_multiline = """ - this is value -""" - -escape_sequences = "yay\nboo" -escaped_newlines = abcd \ -efg -)"; - - auto ini = Ini.ParseString(data); - assert(ini.getKey("key1") == "value"); - assert(ini.getKey("test") == "bar ; comment"); - - assert(ini.hasSection("section 1")); - with (ini["section 1"]) { - assert(getKey("key1") == "new key"); - assert(getKey("num") == "151"); - assert(getKey("empty") == ""); - } - - assert(ini.hasSection("various")); - with (ini["various"]) { - assert(getKey("quoted key") == "VALUE 123"); - assert(getKey("quote_multiline") == "\n this is value\n"); - assert(getKey("escape_sequences") == "yay\nboo"); - assert(getKey("escaped_newlines") == "abcd efg"); - } -} - -unittest { - auto data = q"EOF -key1 = value - -# comment - -test = bar ; comment - -[section 1] -key1 = new key -num = 151 -empty - -EOF"; - - auto ini = Ini.ParseString(data); - assert(ini.getKey("key1") == "value"); - assert(ini.getKey("test") == "bar ; comment"); - assert(ini.hasSection("section 1")); - assert(ini["section 1"]("key1") == "new key"); - assert(ini["section 1"]("num") == "151"); - assert(ini["section 1"]("empty") == ""); -} - -unittest { - auto data = q"EOF -[def] -name1=value1 -name2=value2 - -[foo : def] -name1=Name1 from foo. Lookup for def.name2: %name2% -EOF"; - - // Parse file - auto ini = Ini.ParseString(data, true); - - assert(ini["foo"].getKey("name1") - == "Name1 from foo. Lookup for def.name2: value2"); -} - -unittest { - auto data = q"EOF -[section] -name=%value% -EOF"; - - // Create ini struct instance - Ini ini; - Ini iniSec = IniSection("section"); - ini.addSection(iniSec); - - // Set key value - ini["section"].setKey("value", "verify"); - - // Now, you can use value in ini file - ini.parseString(data); - - assert(ini["section"].getKey("name") == "verify"); -} - - -unittest { - import dini.reader; - - alias MyReader = INIReader!( - UniversalINIFormat, - UniversalINIReader.CurrentFlags & ~INIFlags.ProcessEscapes, - UniversalINIReader.CurrentBoxer - ); - auto ini = Ini.ParseStringWith!MyReader(`path=C:\Path`); - assert(ini("path") == `C:\Path`); -} \ No newline at end of file diff --git a/import/dini/reader.d b/import/dini/reader.d deleted file mode 100644 index 6a03199..0000000 --- a/import/dini/reader.d +++ /dev/null @@ -1,786 +0,0 @@ -/** - * Implements INI reader. - * - * `INIReader` is fairly low-level, configurable reader for reading INI data, - * which you can use to build your own object-model. - * - * High level interface is available in `dini.parser`. - * - * - * Unless you need to change `INIReader` behaviour, you should use one of provided - * preconfigured readers: - * - * - `StrictINIReader` - * - * Lower compatibility, may be bit faster. - * - * - * - `UniversalINIReader` - * - * Higher compatibility, may be slighly slower. - */ -module dini.reader; - -import std.algorithm : countUntil, canFind, map; -import std.array : array; -import std.functional : unaryFun; -import std.string : representation, assumeUTF, strip, - stripLeft, stripRight, split, join, format; -import std.range : ElementType, replace; -import std.uni : isWhite, isSpace; -import std.variant : Algebraic; -import dini.utils : isBoxer, BoxerType, parseEscapeSequences; - - -/** - * Represents type of current token used by INIReader. - */ -enum INIToken -{ - BLANK, /// - SECTION, /// - KEY, /// - COMMENT /// -} - - -/** - * Represents a block definition. - * - * Block definitions are used to define new quote and comment sequences - * to be accepted by INIReader. - * - * BlockDefs can be either single line or multiline. To define new single - * line block `INIBlockDef.mutliline` must be set to `false` AND `closing` - * must be set to newline string(`"\n"`). - */ -struct INIBlockDef -{ - /** - * Opening character sequence - */ - string opening; - - /** - * Closing character sequence - */ - string closing; - - /** - * Should newline characters be allowed? - */ - bool multiline; -} - - -/** - * INIReader behaviour flags. - * - * These flags can be used to modify INIReader behaviour. - */ -enum INIFlags : uint -{ - /** - * Should escape sequences be translated? - */ - ProcessEscapes = 1 << 0, - - - /** - * Section names will be trimmed. - */ - TrimSections = 1 << 4, - - /** - * Key names will be trimmed. - */ - TrimKeys = 1 << 5, - - /** - * Values will be trimmed. - */ - TrimValues = 1 << 6, - - /** - * Section names, keys and values will be trimmed. - */ - TrimAll = TrimSections | TrimKeys | TrimValues -} - - -/** - * Defines INI format. - * - * This struct defines INI comments and quotes sequences. - * - * `INIReader` adds no default quotes or comment definitions, - * and thus when defining custom format make sure to include default - * definitions to increase compatibility. - */ -struct INIFormatDescriptor -{ - /** - * List of comment definitions to support. - */ - INIBlockDef[] comments; - - /** - * List of quote definitions to support. - */ - INIBlockDef[] quotes; -} - - -/** - * Strict INI format. - * - * This format is used by `MinimalINIReader`. - * - * This format defines only `;` as comment character and `"` as only quote. - * For more universal format consider using `UniversalINIFormat`. - */ -const INIFormatDescriptor StrictINIFormat = INIFormatDescriptor( - [INIBlockDef(";", "\n", false)], - [INIBlockDef(`"`, `"`, false)] -); - - -/** - * Universal INI format. - * - * This format extends `StrictINIFormat` with hash-comments (`#`) and multiline - * triple-quotes (`"""`). - */ -const INIFormatDescriptor UniversalINIFormat = INIFormatDescriptor( - [INIBlockDef(";", "\n", false), INIBlockDef("#", "\n", false)], - [INIBlockDef(`"""`, `"""`, true), INIBlockDef(`"`, `"`, false)] -); - - -/** - * Thrown when an parsing error occurred. - */ -class INIException : Exception -{ - this(string msg = null, Throwable next = null) { super(msg, next); } - this(string msg, string file, size_t line, Throwable next = null) { - super(msg, file, line, next); - } -} - - -/** - * Represents parsed INI key. - * - * Prefer using `YOUR_READER.KeyType` alias. - */ -struct INIReaderKey(ValueType) -{ - /** - * Key name - */ - string name; - - /** - * Key value (may be boxed) - */ - ValueType value; -} - - -/** - * Splits source into tokens. - * - * This struct requires token delimeters to be ASCII-characters, - * Unicode is not supported **only** for token delimeters. - * - * Unless you want to modify `INIReader` behaviour prefer using one of available - * preconfigured variants: - * - * - `StrictINIReader` - * - `UniversalINIReader` - * - * - * `INIReader` expects three template arguments: - * - * - `Format` - * - * Instance of `INIFormatDescriptor`, defines quote and comment sequences. - * - * - * - `Flags` - * - * `INIReaderFlags` (can be OR-ed) - * - * - * - `Boxer` - * - * Name of a function that takes `(string value, INIReader reader)` and returns a value. - * By default all readers just proxy values, doing nothing, but this can be used to e.g. - * store token values as JSONValue or other Algebraic-like type. - * - * `INIReader.BoxType` is always return type of boxer function. So if you passed a boxer that - * returns `SomeAlgebraic` then `typeof(reader.key.value)` is `SomeAlgebraic`. - * - * - * Params: - * Format - `INIFormatDescriptor` to use. - * Flags - Reader behaviour flags. - * Boxer - Function name that can optionally box values. - * - * - * Examples: - * --- - * auto reader = UniversalINIReader("key=value\n"); - * - * while (reader.next) { - * writeln(reader.value); - * } - * --- - */ -struct INIReader(INIFormatDescriptor Format, ubyte Flags = 0x00, alias Boxer) - if (isBoxer!Boxer) -{ - /** - * Reader's format descriptor. - */ - alias CurrentFormat = Format; - - /** - * Reader's flags. - */ - alias CurrentFlags = Flags; - - /** - * Reader's boxer. - */ - alias CurrentBoxer = Boxer; - - /** - * Reader's Box type (boxer return type). - */ - alias BoxType = BoxerType!Boxer; - - - /** - * Alias for INIReaderKey!(BoxType). - */ - alias KeyType = INIReaderKey!BoxType; - - /** - * Type of `value` property. - */ - alias TokenValue = Algebraic!(string, KeyType); - - - /** - * INI source bytes. - */ - immutable(ubyte)[] source; - - /** - * INI source offset in bytes. - */ - size_t sourceOffset; - - /** - * Type of current token. - */ - INIToken type; - - /** - * Indicates whenever source has been exhausted. - */ - bool empty; - - /** - * Used only with Key tokens. - * - * Indicates whenever current value has been quoted. - * This information can be used by Boxers to skip boxing of quoted values. - */ - bool isQuoted; - - /** - * Current token's value. - */ - TokenValue value; - - - /** - * Creates new instance of `INIReader` from `source`. - * - * If passed source does not end with newline it is added (and thus allocates). - * To prevent allocation make sure `source` ends with new line. - * - * Params: - * source - INI source. - */ - this(string source) - { - // Make source end with newline - if (source[$-1] != '\n') - this.source = (source ~ "\n").representation; - else - this.source = source.representation; - } - - /** - * Returns key token. - * - * Use this only if you know current token is KEY. - */ - KeyType key() @property { - return value.get!KeyType; - } - - /** - * Returns section name. - * - * Use this only if you know current token is SECTION. - */ - string sectionName() @property { - return value.get!string; - } - - /** - * Reads next token. - * - * Returns: - * True if more tokens are available, false otherwise. - */ - bool next() - { - isQuoted = false; - skipWhitespaces(); - - if (current.length == 0) { - empty = true; - return false; - } - - int pairIndex = -1; - while(source.length - sourceOffset > 0) - { - if (findPair!`comments`(pairIndex)) { - readComment(pairIndex); - break; - } - else if (current[0] == '[') { - readSection(); - break; - } - else if (isWhite(current[0])) { - skipWhitespaces(); - } - else { - readEntry(); - break; - } - } - - return true; - } - - bool findPair(string fieldName)(out int pairIndex) - { - if (source.length - sourceOffset > 0 && sourceOffset > 0 && source[sourceOffset - 1] == '\\') return false; - - alias MemberType = typeof(__traits(getMember, Format, fieldName)); - foreach (size_t i, ElementType!MemberType pairs; __traits(getMember, Format, fieldName)) { - string opening = pairs.tupleof[0]; - - if (source.length - sourceOffset < opening.length) - continue; - - if (current[0..opening.length] == opening) { - pairIndex = cast(int)i; - return true; - } - } - - return false; - } - - void readSection() - { - type = INIToken.SECTION; - auto index = current.countUntil(']'); - if (index == -1) - throw new INIException("Section not closed"); - - value = current[1 .. index].assumeUTF; - - static if (Flags & INIFlags.TrimSections) - value = value.get!string.strip; - - sourceOffset += index + 1; - } - - void readComment(int pairIndex) - { - type = INIToken.COMMENT; - INIBlockDef commentDef = Format.comments[pairIndex]; - sourceOffset += commentDef.opening.length; - - auto index = current.countUntil(commentDef.closing); - if (index == -1) - throw new INIException("Comment not closed"); - - value = current[0.. index].assumeUTF; - - if (commentDef.multiline == false && value.get!string.canFind('\n')) - throw new INIException("Comment not closed (multiline)"); - - sourceOffset += index + commentDef.closing.length; - } - - void readEntry() - { - type = INIToken.KEY; - KeyType key; - - readKey(key); - if (current[0] == '=') { - sourceOffset += 1; - key.value = readValue(); - } - - value = key; - } - - void readKey(out KeyType key) - { - if (tryReadQuote(key.name)) { - isQuoted = true; - return; - } - - auto newLineOffset = current.countUntil('\n'); - if (newLineOffset > 0) { // read untill newline/some assign sequence - auto offset = current[0..newLineOffset].countUntil('='); - - if (offset == -1) - key.name = current[0 .. newLineOffset].assumeUTF; - else - key.name = current[0 .. offset].assumeUTF; - - sourceOffset += key.name.length; - key.name = key.name.stripRight; - - static if (Flags & INIFlags.TrimKeys) - key.name = key.name.stripLeft; - } - } - - - BoxType readValue() - { - auto firstNonSpaceIndex = current.countUntil!(a => !isSpace(a)); - if (firstNonSpaceIndex > 0) - sourceOffset += firstNonSpaceIndex; - - string result = ""; - auto indexBeforeQuotes = sourceOffset; - - isQuoted = tryReadQuote(result); - auto newlineOffset = current.countUntil('\n'); - string remains = current[0..newlineOffset].assumeUTF; - - if (isQuoted && newlineOffset > 0) { - sourceOffset = indexBeforeQuotes; - isQuoted = false; - } - - if (!isQuoted) { - bool escaped = false; - int[] newlineOffsets = []; - auto localOffset = 0; - for (; source.length - sourceOffset > 0; ++localOffset) { - if (source[sourceOffset + localOffset] == '\\') { - escaped = !escaped; - continue; - } - - else if(escaped && source[sourceOffset + localOffset] == '\r') - continue; - - else if(escaped && source[sourceOffset + localOffset] == '\n') - newlineOffsets ~= localOffset; - - else if (!escaped && source[sourceOffset + localOffset] == '\n') - break; - - escaped = false; - } - - result = current[0..localOffset].assumeUTF.split("\n").map!((line) { - line = line.stripRight; - if (line[$-1] == '\\') return line[0..$-1].stripLeft; - return line.stripLeft; - }).array.join(); - sourceOffset += localOffset; - } - - static if (Flags & INIFlags.TrimValues) - if (!isQuoted) - result = result.strip; - - static if (Flags & INIFlags.ProcessEscapes) - result = parseEscapeSequences(result); - - return Boxer(result); - } - - bool tryReadQuote(out string result) - { - int pairIndex; - - if (findPair!`quotes`(pairIndex)) { - auto quote = Format.quotes[pairIndex]; - sourceOffset += quote.opening.length; - - auto closeIndex = current.countUntil(quote.closing); - if (closeIndex == -1) - throw new INIException("Unterminated string literal"); - - result = current[0..closeIndex].assumeUTF; - sourceOffset += result.length + quote.closing.length; - - if (result.canFind('\n') && quote.multiline == false) - throw new INIException("Unterminated string literal which spans multiple lines (invalid quotes used?)"); - - return true; - } - - return false; - } - - void skipWhitespaces() - { - while (current.length && isWhite(current[0])) - sourceOffset += 1; - } - - private immutable(ubyte)[] current() @property { - return source[sourceOffset..$]; - } -} - - -/** - * Universal `INIReader` variant. - * - * Use this variant if you want to have more compatible parser. - * - * Specifics: - * - Uses `UniversalINIFormat`. - * - Trims section names, keys and values. - * - Processes escapes in values (e.g. `\n`). - */ -alias UniversalINIReader = INIReader!(UniversalINIFormat, INIFlags.TrimAll | INIFlags.ProcessEscapes, (string a) => a); - - -/** - * Strict `INIReader` variant. - * - * Use this variant if you want to have more strict (and bit faster) parser. - * - * Specifics: - * - Uses `StrictINIFormat` - * - Only Keys are trimmed. - * - No escape sequences are resolved. - */ -alias StrictINIReader = INIReader!(StrictINIFormat, INIFlags.TrimKeys, (string a) => a); - - -unittest { - auto source = ` -; comment - -multiline = """ - this is -""" - -numeric=-100000 -numeric2=09843 -[section (name)] -@=bar -`; - - - auto reader = UniversalINIReader(source); - alias Key = reader.KeyType; - - assert(reader.next()); - assert(reader.type == INIToken.COMMENT); - assert(reader.sectionName == " comment"); - - assert(reader.next()); - assert(reader.type == INIToken.KEY); - assert(reader.key.name == "multiline"); - assert(reader.key.value == "\n this is\n"); - - assert(reader.next()); - assert(reader.type == INIToken.KEY); - assert(reader.value.get!Key.name == "numeric"); - assert(reader.value.get!Key.value == "-100000"); - - assert(reader.next()); - assert(reader.type == INIToken.KEY); - assert(reader.value.get!Key.name == "numeric2"); - assert(reader.value.get!Key.value == "09843"); - - assert(reader.next()); - assert(reader.type == INIToken.SECTION); - assert(reader.value.get!string == "section (name)"); - - assert(reader.next()); - assert(reader.type == INIToken.KEY); - assert(reader.value.get!Key.name == "@"); - assert(reader.value.get!Key.value == `bar`); - - assert(!reader.next()); -} - - -unittest { - auto source = ` -####### TEST ######## - -numeric value=15 -ThisIsMultilineValue=thisis\ - verylong # comment -"Floating=Value"=1.51 - -[] # comment works -JustAKey -`; - - auto reader = UniversalINIReader(source); - alias Key = reader.KeyType; - - assert(reader.next()); - assert(reader.type == INIToken.COMMENT); - assert(reader.value.get!string == "###### TEST ########"); - - assert(reader.next()); - assert(reader.type == INIToken.KEY); - assert(reader.value.get!Key.name == "numeric value"); - assert(reader.value.get!Key.value == `15`); - - assert(reader.next()); - assert(reader.type == INIToken.KEY); - assert(reader.value.get!Key.name == "ThisIsMultilineValue"); - assert(reader.value.get!Key.value == `thisisverylong # comment`); - - assert(reader.next()); - assert(reader.type == INIToken.KEY); - assert(reader.value.get!Key.name == "Floating=Value"); - assert(reader.value.get!Key.value == `1.51`); - - assert(reader.next()); - assert(reader.type == INIToken.SECTION); - assert(reader.value.get!string == ""); - - assert(reader.next()); - assert(reader.type == INIToken.COMMENT); - assert(reader.value.get!string == " comment works"); - - assert(reader.next()); - assert(reader.type == INIToken.KEY); - assert(reader.value.get!Key.name == "JustAKey"); - assert(reader.value.get!Key.value == null); - - assert(!reader.next()); -} - -unittest { - string source = ` - [ Debug ] -sNumString=10Test -QuotedNum="10" -QuotedFloat="10.1" -Num=10 -Float=10.1 -`; - - auto reader = UniversalINIReader(source); - alias Key = reader.KeyType; - - assert(reader.next()); - assert(reader.type == INIToken.SECTION); - assert(reader.value.get!string == "Debug"); - - assert(reader.next()); - assert(reader.type == INIToken.KEY); - assert(reader.value.get!Key.name == "sNumString"); - assert(reader.value.get!Key.value == `10Test`); - - assert(reader.next()); - assert(reader.type == INIToken.KEY); - assert(reader.value.get!Key.name == "QuotedNum"); - assert(reader.value.get!Key.value == `10`); - - assert(reader.next()); - assert(reader.type == INIToken.KEY); - assert(reader.value.get!Key.name == "QuotedFloat"); - assert(reader.value.get!Key.value == `10.1`); - - assert(reader.next()); - assert(reader.type == INIToken.KEY); - assert(reader.value.get!Key.name == "Num"); - assert(reader.value.get!Key.value == "10"); - - assert(reader.next()); - assert(reader.type == INIToken.KEY); - assert(reader.value.get!Key.name == "Float"); - assert(reader.value.get!Key.value == "10.1"); - - assert(!reader.next()); -} - -unittest { - string source = ` - [ Debug ] -sNumString=10Test -QuotedNum="10" -QuotedFloat="10.1" -Num=10 -Float=10.1 -`; - - auto reader = StrictINIReader(source); - alias Key = reader.KeyType; - - assert(reader.next()); - assert(reader.type == INIToken.SECTION); - assert(reader.value.get!string == " Debug "); - - assert(reader.next()); - assert(reader.type == INIToken.KEY); - assert(reader.value.get!Key.name == "sNumString"); - assert(reader.value.get!Key.value == `10Test`); - - assert(reader.next()); - assert(reader.type == INIToken.KEY); - assert(reader.value.get!Key.name == "QuotedNum"); - assert(reader.value.get!Key.value == `10`); - - assert(reader.next()); - assert(reader.type == INIToken.KEY); - assert(reader.value.get!Key.name == "QuotedFloat"); - assert(reader.value.get!Key.value == `10.1`); - - assert(reader.next()); - assert(reader.type == INIToken.KEY); - assert(reader.value.get!Key.name == "Num"); - assert(reader.value.get!Key.value == `10`); - - assert(reader.next()); - assert(reader.type == INIToken.KEY); - assert(reader.value.get!Key.name == "Float"); - assert(reader.value.get!Key.value == `10.1`); - - assert(!reader.next()); -} \ No newline at end of file diff --git a/import/dini/utils.d b/import/dini/utils.d deleted file mode 100644 index 9dc9cf2..0000000 --- a/import/dini/utils.d +++ /dev/null @@ -1,66 +0,0 @@ -module dini.utils; - -import std.format : format, formatElement, FormatSpec, FormatException, formattedRead; -import std.traits : arity, isCallable, Parameters, ReturnType; - - -enum bool isBoxer(alias boxer) = isCallable!boxer - && arity!boxer == 1 - && is(Parameters!boxer[0] == string); - -alias BoxerType(alias boxer) = ReturnType!boxer; - - -static char[char] escapeSequences; -static this() { - escapeSequences = [ - 'n': '\n', 'r': '\r', 't': '\t', 'b': '\b', '\\': '\\', - '#': '#', ';': ';', '=': '=', ':': ':', '"': '"', '\'': '\'' - ]; -} - -string parseEscapeSequences(string input) -{ - bool inEscape; - const(char)[] result = []; - result.reserve(input.length); - - for(auto i = 0; i < input.length; i++) { - char c = input[i]; - - if (inEscape) { - if (c in escapeSequences) - result ~= escapeSequences[c]; - else if (c == 'x') { - ubyte n; - if (i + 3 > input.length) - throw new FormatException("Invalid escape sequence (\\x)"); - string s = input[i+1..i+3]; - if (formattedRead(s, "%x", &n) < 1) - throw new FormatException("Invalid escape sequence (\\x)"); - result ~= cast(char)n; - i += 2; - } - else { - throw new FormatException("Invalid escape sequence (\\%s..)".format(c)); - } - } - else if (!inEscape && c == '\\') { - inEscape = true; - continue; - } - else result ~= c; - - inEscape = false; - } - - return cast(string)result; -} - -unittest { - assert(parseEscapeSequences("abc wef ' n r ;a") == "abc wef ' n r ;a"); - assert(parseEscapeSequences(`\\n \\\\\\\\\\r`) == `\n \\\\\r`); - assert(parseEscapeSequences(`hello\nworld`) == "hello\nworld"); - assert(parseEscapeSequences(`multi\r\nline \#notacomment`) == "multi\r\nline #notacomment"); - assert(parseEscapeSequences(`welp \x5A\x41\x7a`) == "welp ZAz"); -} diff --git a/initial.d b/initial.d new file mode 100644 index 0000000..7f4a585 --- /dev/null +++ b/initial.d @@ -0,0 +1,454 @@ +/* + * Copyright (c) 2024 Jeremy Baxter. All rights reserved. + * + * Boost Software License - Version 1.0 - August 17th, 2003 + * + * Permission is hereby granted, free of charge, to any person or organization + * obtaining a copy of the software and accompanying documentation covered by + * this license (the "Software") to use, reproduce, display, distribute, + * execute, and transmit the Software, and to prepare derivative works of the + * Software, and to permit third-parties to whom the Software is furnished to + * do so, all subject to the following: + * + * The copyright notices in the Software and this entire statement, including + * the above license grant, this restriction and the following disclaimer, + * must be included in all copies of the Software, in whole or in part, and + * all derivative works of the Software, unless such copies or derivative + * works are solely in the form of machine-executable object code generated by + * a source language processor. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT + * SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE + * FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, + * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +/++ + + INI is a simple, concise configuration or data interchange + + format. It consists of keys and values listed inside a section. + + An INI document might look something like this: + + --- + + [user] + + name = Kevin + + age = 26 + + emailAddress = kevin@example.net + + + + [config] + + useBundledMalloc = false + + forceDefaultInit = true + + --- + + + + $(B initial) is a library for parsing INI documents into an `INIUnit`, + + a data structure representing an INI document. The function `readINI` + + contains the parser; it parses the INI given to it and deserialises + + it into the given `INIUnit`. The `readINIFile` function can be used + + to read the INI from a file rather than a string. + + + + $(B initial) is fully @safe, and can be used in @safe code. + + + + License: [Boost License 1.0](https://www.boost.org/LICENSE_1_0.txt). + + Authors: Jeremy Baxter + +/ +module initial; + +import std.conv : to, ConvException; +import std.exception : basicExceptionCtors, enforce; +import std.format : format; +import std.traits : isSomeChar; + +private alias parserEnforce = enforce!INIParseException; + +@safe: + +/++ + + Structure that represents an INI file. + + + + Contains `INISection`s in an associative array that can be + + directly accessed if needs be, or the index operator can be + + used to get an `INISection` and automatically initialise it + + if needed. + + + + The `serialise` method can be used to serialise the entire + + structure into an INI document that can be then read again + + by the parser and modified by humans. + + + + Example: + + --- + + INIUnit ini; + + + + /* write INI data */ + + ini["section"]["key"] = "value"; + + ini["section"]["num"] = "4.8"; + + + + /* read INI data */ + + readINI(ini, ` + + [section] + + num = 5.3 + + `); + + + + /* read INI from file */ + + readINIFile(ini, "config.ini"); + + + + /* write INI to file */ + + import std.file : write; + + write("config.ini", ini.serialise()); + + --- + +/ +struct INIUnit +{ + INISection[string] sections; /++ Hashmap of INISections in this unit. +/ + string defaultSection = "main"; /++ Name of the default section. +/ + + nothrow: + + /++ + + Returns the value of *k* in the default section + + or *defaultValue* if *k* is not present. + +/ + string + key(string k, string defaultValue = null) + { + return this[defaultSection].key(k, defaultValue); + } + + /++ + + Serialises this `INIUnit` into an INI document. + + + + If *defaultSectionHeading* is true, includes the + + section heading of the default section. + + Example: + + --- + + INIUnit ini; + + + + ini["sections"] = "ages suburbs"; + + ini["ages"]["John"] = "37"; + + ini["ages"]["Mary"] = "29"; + + ini["suburbs"]["John"] = "Gordonton"; + + ini["suburbs"]["Mary"] = "Ashmore"; + + + + writeln(ini.serialise()); + + /* + + * sections = ages suburbs + + * [ages] + + * John = 37 + + * Mary = 29 + + * [suburbs] + + * John = Gordonton + + * Mary = Ashmore + + */ + + --- + +/ + char[] + serialise(bool defaultSectionHeading = false) + { + char[] buf; + + buf = this[defaultSection].serialise(defaultSectionHeading); + foreach (string sect; sections.byKey()) { + if (sect == defaultSection) + continue; + + buf ~= this[sect].serialise(); + } + + return buf; + } + + /++ + + Returns the `INISection` associated with the name *sect*. + + Initialises it if it doesn't exist. + +/ + ref INISection + opIndex(string sect) + { + if (!(sect in sections)) + sections[sect] = INISection(sect); + + return sections[sect]; + } + + /++ + + Sets the value of the key *k* in the default section to *v*. + + Example: + + --- + + ini["key"] = "value"; + + --- + +/ + void + opIndexAssign(string v, string k) + { + sections[defaultSection][k] = v; + } + + /++ + + Sets the value of the `INISection` named *sect*. + +/ + void + opIndexAssign(INISection v, string sect) + { + sections[sect] = v; + } +} + +/++ + + Represents an INI section. + + + + Holds keys in the form of an associative array. + + Index the `keys` property to get this data raw, or to avoid + + range errors, use the `key` or `keyAs` functions. + + + + `alias this` is applied to the `keys` associative array, + + meaning you can index the structure to get or set keys' values. + +/ +struct INISection +{ + string[string] keys; /++ Hashmap of keys belonging to this section. +/ + string name; /++ Name of this section. +/ + + /++ + + Construct a new `INISection` with the given name. + + Called by `readINI` and `INIUnit.opIndex`. + +/ + this(string name) nothrow + { + this.name = name; + keys = new string[string]; + } + + /+ + + Returns the value of `k` in this section or + + `defaultValue` if the key is not present. + +/ + string + key(string k, string defaultValue = null) nothrow + { + return k in keys ? keys[k] : defaultValue; + } + + /++ + + Using `std.conv`, converts the value of *k* to *T*. + + On conversion failure throws an `INITypeException` + + with a message containing location information. + +/ + T + keyAs(T)(string k, string defaultValue = null) + { + try + return key(k, defaultValue).to!T(); + catch (ConvException) + throw new INITypeException( + format!"unable to convert [%s].%s to %s"(name, k, T.stringof)); + } + + /++ + + Serialise this section into an INI document. + + + + If `sectionHeading` is true, includes the section heading at the top. + +/ + char[] + serialise(bool sectionHeading = true) nothrow + { + char[] buf; + + if (sectionHeading) + buf ~= "[" ~ name.to!(char[]) ~ "]\n"; + + foreach (string key; keys.byKey()) { + buf ~= key ~ " = " ~ keys[key] ~ "\n"; + } + + return buf; + } + + alias keys this; +} + +/++ + + Exception thrown when parsing failure occurs. + +/ +class INIParseException : Exception +{ + mixin basicExceptionCtors; +} + +/++ + + Exception thrown when conversion failure occurs. + +/ +class INITypeException : Exception +{ + mixin basicExceptionCtors; +} + +private string +locate(size_t l, size_t c) +{ + return format!"%d:%d"(l, c); +} + +/++ + + Parse the given INI data into an `INIUnit`. + + + + To parse a file's contents, use readINIFile instead. + + + + Whitespace is first stripped from the beginning and end of + + the line before parsing it. + + + + The following rules apply when parsing: + + $(UL + + $(LI if a hash character `#` is found at the beginning of a line, + + the rest of the line is ignored) + + $(LI if a left square bracket character `[` is found at + + the beginning of a line, the line is considered a + + section heading) + + $(LI inside a section heading, any character can be used + + inside the section heading except the right square bracket + + character) + + $(LI once the right square bracket character `]` is found, + + the section heading is considered finished. Any trailing + + characters are garbage and will trigger a parse error.) + + $(LI if a character except the left square bracket is found + + at the beginning of a line, the line is considered an + + assignment in the current section) + + $(LI once an equals character `=` is found after the first + + part of an assignment, the rest of the line is considered + + the "value" while the first part is the "key") + + $(LI if whitespace is found in the first part of an assignment, + + it is ignored and the next equals sign is looked for + + immediately) + + $(LI if whitespace is found after an equals character `=` + + in an assignment, it is ignored until the first + + non-whitespace character is found) + + $(LI once a non-whitespace character is found after an equals + + character `=` in an assignment, it is counted part of the + + "value") + +/ +void +readINI(ref INIUnit ini, string data) +{ + import std.string : splitLines, strip; + + string section; /* name of current section */ + string text; /* current key, value, section, whatever */ + string key; /* name of key in section */ + string value; /* value */ + bool inAssign; /* in key assignment? */ + bool inHeading; /* in section heading? */ + bool needEqls; /* require equals sign immediately? */ + bool pastEqls; /* past equals sign in assignment? */ + bool trailing; /* trailing garbage after heading? */ + + section = ini.defaultSection; + + nextline: + foreach (size_t ln, string line; data.splitLines()) { + line = line.strip(); + if (line.length == 0) + continue; + + inAssign = inHeading = needEqls = pastEqls = trailing = false; + text = key = value = ""; + foreach (size_t cn, char ch; line) { + switch (ch) { + /* comment? */ + case '#': + if (cn == 0) + continue nextline; + + text ~= ch; + break; + + /* beginning of a section heading? */ + case '[': + if (cn == 0) /* beginning of line? */ + inHeading = true; + else + text ~= ch; + break; + + /* end of a section heading? */ + case ']': + parserEnforce(inHeading || inAssign, + locate(ln, cn) ~ ": unexpected character ']'"); + + if (inHeading) { + section = text; + text = ""; + inHeading = false; + trailing = true; + } else if (inAssign) { + text ~= ch; + } + break; + + /* middle of an assignment? */ + case '=': + if (inAssign) { + if (pastEqls) { + /* count it as part of the value */ + text ~= ch; + } else { + key = text; + text = ""; + pastEqls = true; + needEqls = false; + } + } else { + goto default; + } + break; + + /* whitespace */ + case ' ': + case '\t': + case '\v': + case '\r': + case '\n': + case '\f': + if (inAssign) { + if (!pastEqls) + needEqls = true; + } + break; + + default: + if (cn == 0) /* beginning of line? */ + inAssign = true; + + parserEnforce((inAssign || inHeading) && !trailing, + locate(ln, cn) ~ ": unexpected character '" ~ ch ~ "'"); + + if (inAssign) + parserEnforce(!needEqls, + locate(ln, cn) ~ ": expected '=', not '" ~ ch ~ "'"); + + text ~= ch; + break; + } + } + /* once the line has finished being looped over... */ + if (inAssign) { + parserEnforce(key.length != 0, + locate(ln, line.length - 1) ~ ": key cannot be empty"); + value = text; + text = ""; + + if (!(section in ini.sections)) + ini[section] = INISection(section); + ini[section][key] = value; + } + } +} + +/++ + + Parse INI from a file located at `fileName`. + +/ +void +readINIFile(ref INIUnit ini, string fileName) +{ + import std.file : readText; + + readINI(ini, readText(fileName)); +} From 8b392c5baba9379c1f8eab0e56f73925f68edc22 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Tue, 13 Feb 2024 09:40:44 +1300 Subject: [PATCH 039/133] use @safe everywhere with a little bit of evil @trusted magic... --- esv.d | 9 ++++++++- esvapi.d | 36 ++++++++++++++++++++---------------- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/esv.d b/esv.d index a03c586..fc8c3b1 100644 --- a/esv.d +++ b/esv.d @@ -29,7 +29,7 @@ import std.getopt : getoptConfig = config; import std.path : baseName, dirName, expandTilde, isValidPath; import std.process : environment, executeShell; import std.regex : regex, matchFirst, replaceAll, replaceFirst; -import std.stdio : writef, writeln, writefln, stderr; +import std.stdio : writef, writeln, writefln, File; import std.string : splitLines; import initial; @@ -37,6 +37,8 @@ import initial; import config; import esvapi; +@safe: + enum VERSION = "0.2.0"; bool aFlag; /* audio */ @@ -49,6 +51,8 @@ bool rFlag, RFlag; /* passage references */ string sFlag; /* search passages */ bool VFlag; /* show version */ +File stderr; + version (OpenBSD) { immutable(char) *promises; } @@ -58,6 +62,9 @@ main(string[] args) { bool success; + /* @safe way of opening stderr on Unix */ + stderr = File("/dev/stderr", "w"); + version (OpenBSD) { import core.sys.openbsd.unistd : pledge; import std.string : toStringz; diff --git a/esvapi.d b/esvapi.d index 79e70f5..2178258 100644 --- a/esvapi.d +++ b/esvapi.d @@ -30,6 +30,8 @@ import std.stdio : File; import std.string : capitalize, tr, wrap; import std.net.curl : HTTP; +@safe: + /** Indentation style to use when formatting passages. */ enum ESVIndent { @@ -141,7 +143,7 @@ const string[] ESVAPI_PARAMETERS = [ * Otherwise, returns false. */ bool -bookValid(in char[] book) nothrow @safe +bookValid(in char[] book) nothrow { foreach (string b; BIBLE_BOOKS) { if (book.capitalize() == b.capitalize()) @@ -154,7 +156,7 @@ bookValid(in char[] book) nothrow @safe * Otherwise, returns false. */ bool -verseValid(in char[] verse) @safe +verseValid(in char[] verse) { foreach (string re; [ "^\\d{1,3}$", @@ -193,7 +195,7 @@ class ESVApi /** * Constructs an ESVApi object using the given authentication key. */ - this(string key, string tmpName = "esv") @safe + this(string key, string tmpName = "esv") { _key = key; _tmp = tempDir() ~ tmpName; @@ -212,7 +214,7 @@ class ESVApi * was constructed. This authentication key cannot be changed. */ final @property string - key() const nothrow pure @nogc @safe + key() const nothrow pure @nogc { return _key; } @@ -220,7 +222,7 @@ class ESVApi * Returns the subdirectory used to store temporary audio passages. */ final @property string - tmpDir() const nothrow pure @nogc @safe + tmpDir() const nothrow pure @nogc { return _tmp; } @@ -228,7 +230,7 @@ class ESVApi * Returns the API URL currently in use. */ final @property string - url() const nothrow pure @nogc @safe + url() const nothrow pure @nogc { return _url; } @@ -236,7 +238,7 @@ class ESVApi * Sets the API URL currently in use to the given url argument. */ final @property void - url(immutable(string) url) @safe + url(immutable(string) url) in (!url.matchAll(`^https?://.+\\..+(/.+)?`).empty, "Invalid URL format") { _url = url; @@ -348,19 +350,21 @@ class ESVApi lineLength = lineLength == 0 ? 80 : lineLength; - foreach (JSONValue item; resp["results"].array) { - layout ~= format!fmt( - item["reference"].str, - item["content"].str - .wrap(lineLength) - ); - } + () @trusted { + foreach (JSONValue item; resp["results"].array) { + layout ~= format!fmt( + item["reference"].str, + item["content"].str + .wrap(lineLength) + ); + } + }(); return layout; } protected char[] - makeRequest(in char[] query) + makeRequest(in char[] query) @trusted { char[] response; HTTP request; @@ -394,7 +398,7 @@ struct ESVApiOptions * If initialise is true, initialise an ESVApiOptions * structure with the default values. */ - this(bool initialise) nothrow @safe + this(bool initialise) nothrow { if (!initialise) return; From 566ec4533776a12fc02dbde12b46674137d405bf Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Tue, 13 Feb 2024 09:50:50 +1300 Subject: [PATCH 040/133] fix install target of makefile --- configure | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/configure b/configure index deb818c..e3a908a 100755 --- a/configure +++ b/configure @@ -31,10 +31,10 @@ esv: ${OBJS} clean: rm -f esv ${OBJS} ${INIOBJS} -install: esv - install -m755 esv ${DESTDIR}${PREFIX}/bin/esv - cp -f esv.1 ${DESTDIR}${MANPREFIX}/man1 - cp -f esv.conf.5 ${DESTDIR}${MANPREFIX}/man5 +install: + install -Dm755 esv ${DESTDIR}${PREFIX}/bin/esv + install -Dm644 esv.1 ${DESTDIR}${MANPREFIX}/man1/esv.1 + install -Dm644 esv.conf.5 ${DESTDIR}${MANPREFIX}/man5/esv.conf.5 .PHONY: all clean install ' From 87826cba0de203031f959f36580a4ed75a20ed40 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Tue, 13 Feb 2024 09:51:25 +1300 Subject: [PATCH 041/133] add nix flake --- .editorconfig | 6 +++++- .gitignore | 2 ++ flake.lock | 27 +++++++++++++++++++++++++++ flake.nix | 35 +++++++++++++++++++++++++++++++++++ 4 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 flake.lock create mode 100644 flake.nix diff --git a/.editorconfig b/.editorconfig index 5ad5271..f3bedeb 100644 --- a/.editorconfig +++ b/.editorconfig @@ -3,4 +3,8 @@ root = true [*] end_of_line = lf indent_style = tab -indent_size = 4 \ No newline at end of file +indent_size = 4 + +[*.nix] +indent_style = space +indent_size = 2 \ No newline at end of file diff --git a/.gitignore b/.gitignore index ad9dd2d..04c1e2b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,6 @@ *.so *.a esv +result + Makefile diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..502c992 --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1707743206, + "narHash": "sha256-AehgH64b28yKobC/DAWYZWkJBxL/vP83vkY+ag2Hhy4=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "2d627a2a704708673e56346fcb13d25344b8eaf3", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..2bcb076 --- /dev/null +++ b/flake.nix @@ -0,0 +1,35 @@ +{ + description = "read the ESV Bible from your terminal"; + + inputs = { + nixpkgs.url = github:NixOS/nixpkgs/nixpkgs-unstable; + }; + + outputs = { self, nixpkgs }: + with nixpkgs.lib; + let + forAllSystems = fn: + genAttrs platforms.unix (system: + fn (import nixpkgs { + inherit system; + }) + ); + in + { + packages = forAllSystems (pkgs: { + default = pkgs.stdenv.mkDerivation (with pkgs; { + name = "esv"; + src = ./.; + + nativeBuildInputs = [ ldc ]; + buildInputs = [ curl ]; + + dontAddPrefix = true; + installFlags = [ + "DESTDIR=$(out)" + "PREFIX=/" + ]; + }); + }); + }; +} From 49e2606a2e881538b2f777fb618268b81620664b Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Tue, 13 Feb 2024 10:09:18 +1300 Subject: [PATCH 042/133] improve error message system Move away from the exception-based system where we throw an Exception to stop the program (which is caught by main) and use a new die function that accesses args[0], prints an error and stops the program. Users should not notice a difference, I think this system is more lightweight and requires less code. --- esv.d | 162 ++++++++++++++++++++++++++++++---------------------------- 1 file changed, 84 insertions(+), 78 deletions(-) diff --git a/esv.d b/esv.d index fc8c3b1..f9429ed 100644 --- a/esv.d +++ b/esv.d @@ -20,15 +20,13 @@ module esv; +import std.algorithm : startsWith; import std.conv : to, ConvException; -import std.exception : enforce; import std.file : exists, mkdirRecurse, write, FileException; import std.format : format; import std.getopt : getopt, GetOptException; -import std.getopt : getoptConfig = config; import std.path : baseName, dirName, expandTilde, isValidPath; import std.process : environment, executeShell; -import std.regex : regex, matchFirst, replaceAll, replaceFirst; import std.stdio : writef, writeln, writefln, File; import std.string : splitLines; @@ -51,6 +49,7 @@ bool rFlag, RFlag; /* passage references */ string sFlag; /* search passages */ bool VFlag; /* show version */ +string[] mainArgs; File stderr; version (OpenBSD) { @@ -59,55 +58,31 @@ version (OpenBSD) { int main(string[] args) -{ - bool success; - - /* @safe way of opening stderr on Unix */ - stderr = File("/dev/stderr", "w"); - - version (OpenBSD) { - import core.sys.openbsd.unistd : pledge; - import std.string : toStringz; - - promises = toStringz("stdio rpath wpath cpath inet dns proc exec prot_exec"); - pledge(promises, null); - } - - debug { - return run(args) ? 0 : 1; - } - - try { - success = run(args); - } catch (Exception e) { - if (typeid(e) == typeid(Exception)) { - stderr.writefln("%s: %s", args[0].baseName(), e.msg); - } else { - stderr.writefln("%s: uncaught %s in %s:%d: %s", - args[0].baseName(), - typeid(e).name, - e.file, e.line, e.msg); - stderr.writefln("this is probably a bug; it would be greatly appreciated if you reported it at:\n\n %s", - BUGREPORTURL); - } - } - - return success ? 0 : 1; -} - -bool -run(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 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 : cfg = config; getopt(args, - getoptConfig.bundling, - getoptConfig.caseSensitive, + cfg.bundling, + cfg.caseSensitive, "a", &aFlag, "C", &CFlag, "F", &FFlag, "f", &fFlag, @@ -119,20 +94,19 @@ run(string[] args) "V", &VFlag, ); } catch (GetOptException e) { - enforce(e.msg.matchFirst(regex("^Unrecognized option")).empty, + enforceDie(!e.msg.startsWith("Unrecognized option"), "unknown option " ~ e.extractOpt()); - enforce(e.msg.matchFirst(regex("^Missing value for argument")).empty, + enforceDie(!e.msg.startsWith("Missing value for argument"), "missing argument for option " ~ e.extractOpt()); - throw new Exception(e.msg); /* catch-all */ + die(e.msg); /* catch-all */ } catch (ConvException e) { - throw new Exception( - "illegal argument to -l option -- must be integer"); + die("illegal argument to -l option -- must be integer"); } if (VFlag) { writeln("esv version " ~ VERSION); - return true; + return 0; } if (sFlag != "") { @@ -142,26 +116,25 @@ run(string[] args) if (args.length < 3) { stderr.writefln("usage: %s [-aFfHhNnRrV] [-C config] [-l length] [-s query] book verses", args[0].baseName()); - return false; + return 1; } - enforce(bookValid(args[1].parseBook()), - format!"book '%s' does not exist"(args[1])); - enforce(verseValid(args[2]), - format!"invalid verse format '%s'"(args[2])); + 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(ENV_CONFIG, DEFAULT_CONFIGPATH) - .expandTilde(); + configPath = environment.get(ENV_CONFIG, DEFAULT_CONFIGPATH).expandTilde(); try { if (CFlag != "") { /* if -C was given */ - enforce(isValidPath(CFlag), CFlag ~ ": invalid path"); + enforceDie(isValidPath(CFlag), CFlag ~ ": invalid path"); configPath = CFlag.expandTilde(); } else { - enforce(isValidPath(configPath), + enforceDie(isValidPath(configPath), configPath ~ ": invalid path"); if (!configPath.exists()) { @@ -176,8 +149,8 @@ key = %s # custom API parameters using `parameters`: #parameters = &my-parameter=value -# Some other settings that modify how the passages are displayed: -#[passage] +# Settings that modify how passages are displayed: +[passage] #footnotes = false #headings = false #passage-references = false @@ -188,16 +161,17 @@ key = %s readINIFile(ini, configPath); } catch (FileException e) { /* filesystem syscall errors */ - throw new Exception(e.msg); + die(e.msg); } + enforceDie(!(aFlag && sFlag), "cannot specify both -a and -s flags"); + apiKey = ini["api"].key("key"); - enforce(apiKey != null, + enforceDie(apiKey != null, "API key not present in configuration file; cannot proceed"); esv = new ESVApi(apiKey); - enforce(!(aFlag && sFlag), "cannot specify both -a and -s options"); if (aFlag) { string tmpf, mpegPlayer; @@ -205,12 +179,11 @@ key = %s mpegPlayer = environment.get(ENV_PLAYER, DEFAULT_MPEGPLAYER); /* check for an audio player */ - enforce( + enforceDie( executeShell( format!"command -v %s >/dev/null 2>&1"(mpegPlayer) ).status == 0, - format!"%s is required for audio mode; cannot continue"(mpegPlayer) - ); + mpegPlayer ~ " is required for audio mode; cannot continue"); /* esv has built-in support for mpg123 and mpv. * other players will work, just recompile with @@ -219,32 +192,37 @@ key = %s mpegPlayer ~= mpegPlayer == "mpg123" ? " -q " : mpegPlayer == "mpv" ? " --msg-level=all=no " : " "; - /* spawn mpg123 */ + /* spawn the player */ executeShell(mpegPlayer ~ tmpf); - return true; + return 0; } + if (sFlag) { writeln(esv.searchFormat(sFlag)); - return true; + return 0; } esv.extraParameters = ini["api"].key("parameters", ""); /* Get [passage] keys */ - foreach (string key; ["footnotes", "headings", "passage-references", "verse-numbers"]) { + foreach (string key; [ + "footnotes", + "headings", + "passage-references", + "verse-numbers" + ]) { try esv.opts.b["include-" ~ key] = ini["passage"].keyAs!bool(key, "true"); catch (INITypeException e) - throw new Exception(configPath ~ ": " ~ e.msg); + die(configPath ~ ": " ~ e.msg); } /* Get line-length ([passage]) */ - try { + try esv.opts.i["line-length"] = ini["passage"].keyAs!int("line-length", "0"); - } catch (INITypeException e) { - throw new Exception(configPath ~ ": " ~ e.msg); - } + catch (INITypeException e) + die(configPath ~ ": " ~ e.msg); if (fFlag) esv.opts.b["include-footnotes"] = true; if (hFlag) esv.opts.b["include-headings"] = true; @@ -257,17 +235,45 @@ key = %s if (lFlag != 0) esv.opts.i["line-length"] = lFlag; writeln(esv.getPassage(args[1].parseBook(), args[2])); - return true; + 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 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) @safe +extractOpt(in GetOptException e) { + import std.regex : matchFirst; + return e.msg.matchFirst("-.")[0]; } private string -parseBook(in string book) @safe +parseBook(in string book) { import std.string : tr; From 8881e201783a10e45ae05409e8518d274dca0ec1 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Tue, 13 Feb 2024 10:24:00 +1300 Subject: [PATCH 043/133] update readme --- README.md | 73 +++++++++++++++++++++++++------------------------------ 1 file changed, 33 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index c3b8b74..536af6a 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,9 @@ -# esv +## esv - read the Bible from your terminal -*Read the Bible from your terminal* - -`esv` is a utility that displays passages of the English Standard Bible -on your terminal. It connects to the ESV web API to retrieve the passages, -and allows configuration through command-line options and the configuration file. +`esv` displays passages of the English Standard Bible on your +terminal. It connects to the ESV web API to retrieve the passages, and +allows configuration through command-line options and the +configuration file. Example usage: @@ -20,59 +19,53 @@ A Psalm of David. He makes me lie down in green pastures.... ``` -The names of Bible books are not case sensitive, so John, john, and JOHN -are all accepted. +The names of Bible books are not case sensitive, so John, john, and +JOHN are all accepted. ## Audio -`esv` supports playing audio passages through the -a option. -The `mpg123` audio/video player is utilised here and so it required if you -want to play audio passages. If you prefer, you can use a different player -(such as mpv) by editing config.di. +esv supports playing audio passages through the -a option. The +`mpg123` audio/video player is utilised here and so it is required if +you want to play audio passages. If you prefer, you can use a +different player (such as mpv) by editing config.di before compiling. -Audio usage is the same as normal text usage. `esv -a Matthew 5-7` will play -an audio passage of Matthew 5-7. +Using the program to play audio is mostly the same as normal text +usage. `esv -a Matthew 5-7` will play an audio passage of Matthew 5-7. ## Installation -To install `esv`, first make sure you have the -[LLVM D compiler (ldc)](https://github.com/ldc-developers/ldc#installation) -installed on your system. +To install esv, first make sure you have a D compiler installed on +your system. +ldc, the LLVM D compiler (https://github.com/ldc-developers/ldc#installation) +is recommended but dmd is also supported. -Commands prefixed with a dollar sign ($) are intended to be run as -a standard user, and commands prefixed with a hash sign (#) are intended +Commands prefixed with a dollar sign ($) are intended to be run as a +standard user, and commands prefixed with a hash sign (#) are intended to be run as the root user. -First, get the source code: +First, download the source code: -``` -$ git clone https://codeberg.org/jtbx/esv -$ cd esv -``` + $ git clone https://github.com/jtbx/esv + $ cd esv Now, compile and install: -``` -$ ./configure -$ make -# make install -``` + $ ./configure + $ make + # make install ## Documentation -All documentation is contained in the manual pages. To access them, you can run -`man esv` and `man esv.conf` for the `esv` utility and the configuration file respectively. +All documentation is contained in the manual pages. To access them, +you can run `man esv` and `man esv.conf` for the `esv` utility and the +configuration file respectively. ## Copying -Copying, modifying and redistributing this software is permitted -as long as your modified version conforms to the GNU General Public License version 2. +Copying, modifying and redistributing this software is permitted as +long as your modified version conforms to the GNU General Public +License version 2. -The file esvapi.d is a reusable library; all documentation is provided in the source file. +Full licenses are contained in the COPYING file. -The license is contained in the file COPYING. - -This software uses a modified version of a library named "dini". This is released under -the Boost Software License version 1.0, which can be found in import/dini/LICENSE. -dini can be found at https://github.com/robik/dini. -My changes can be found at https://github.com/jtbx/dini. +Copyright (c) 2024 Jeremy Baxter From 0752c9db7c1b370036d30834837b76ee956b406f Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Fri, 16 Feb 2024 12:08:52 +1300 Subject: [PATCH 044/133] style fixes o fix doc comment style: use /++ rather than /** just to make it a bit more clear o remove trailing whitespace o remove unnecessary whitespace o use => style for @property functions o more fixes --- esvapi.d | 225 ++++++++++++++++++++++++++----------------------------- 1 file changed, 108 insertions(+), 117 deletions(-) diff --git a/esvapi.d b/esvapi.d index 2178258..6b09644 100644 --- a/esvapi.d +++ b/esvapi.d @@ -3,17 +3,17 @@ * * The GPLv2 License (GPLv2) * Copyright (c) 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 . */ @@ -32,18 +32,18 @@ import std.net.curl : HTTP; @safe: -/** Indentation style to use when formatting passages. */ +/++ Indentation style to use when formatting passages. +/ enum ESVIndent { SPACE, TAB } -/** Default URL to use when sending API requests. */ -enum string ESVAPI_URL = "https://api.esv.org/v3/passage"; +/++ Default URL to use when sending API requests. +/ +enum ESVAPI_URL = "https://api.esv.org/v3/passage"; -/** Constant array of all books in the Bible. */ -const string[] BIBLE_BOOKS = [ +/++ Constant array of all books in the Bible. +/ +immutable string[] BIBLE_BOOKS = [ /* Old Testament */ "Genesis", "Exodus", @@ -115,8 +115,8 @@ const string[] BIBLE_BOOKS = [ "Revelation" ]; -/** All allowed API parameters (for text passages). */ -const string[] ESVAPI_PARAMETERS = [ +/++ All allowed API parameters (for text passages). +/ +immutable string[] ESVAPI_PARAMETERS = [ "include-passage-references", "include-verse-numbers", "include-first-verse-numbers", @@ -138,10 +138,10 @@ const string[] ESVAPI_PARAMETERS = [ "indent-using", ]; -/** - * Returns true if the argument book is a valid book of the Bible. - * Otherwise, returns false. - */ +/++ + + Returns true if the argument book is a valid book of the Bible. + + Otherwise, returns false. + +/ bool bookValid(in char[] book) nothrow { @@ -151,10 +151,10 @@ bookValid(in char[] book) nothrow } return false; } -/** - * Returns true if the argument verse is a valid verse format. - * Otherwise, returns false. - */ +/++ + + Returns true if the argument verse is a valid verse format. + + Otherwise, returns false. + +/ bool verseValid(in char[] verse) { @@ -171,12 +171,12 @@ verseValid(in char[] verse) return false; } -/** - * ESV API object containing the authentication key, - * the API URL, any parameters to use when contacting the - * API as well as the temporary directory to use when - * fetching audio passages. - */ +/++ + + ESV API object containing the authentication key, + + the API URL, any parameters to use when contacting the + + API as well as the temporary directory to use when + + fetching audio passages. + +/ class ESVApi { protected { @@ -187,14 +187,14 @@ class ESVApi ESVApiOptions opts; - /** Additional request parameters */ + /++ Additional request parameters +/ string extraParameters; - /** Called whenever progress is made on a request. */ + /++ Called whenever progress is made on a request. +/ int delegate(size_t, size_t, size_t, size_t) onProgress; - /** - * Constructs an ESVApi object using the given authentication key. - */ + /++ + + Constructs an ESVApi object using the given authentication key. + +/ this(string key, string tmpName = "esv") { _key = key; @@ -209,48 +209,43 @@ class ESVApi }; } - /** - * Returns the API authentication key that was given when the object - * was constructed. This authentication key cannot be changed. - */ + /++ + + Returns the API authentication key that was given when the object + + was constructed. This authentication key cannot be changed. + +/ final @property string key() const nothrow pure @nogc - { - return _key; - } - /** - * Returns the subdirectory used to store temporary audio passages. - */ + => _key; + /++ + + Returns the subdirectory used to store temporary audio passages. + +/ final @property string tmpDir() const nothrow pure @nogc - { - return _tmp; - } - /** - * Returns the API URL currently in use. - */ + => _tmp; + /++ + + Returns the API URL currently in use. + +/ final @property string url() const nothrow pure @nogc - { - return _url; - } - /** - * Sets the API URL currently in use to the given url argument. - */ + => _url; + /++ + + Sets the API URL currently in use to the given url argument. + +/ final @property void url(immutable(string) url) in (!url.matchAll(`^https?://.+\\..+(/.+)?`).empty, "Invalid URL format") { _url = url; } - /** - * Requests the passage in text format from the API and returns it. - * The (case-insensitive) name of the book being searched are - * contained in the argument book. The verse(s) being looked up are - * contained in the argument verses. - * - * Example: getPassage("John", "3:16-21") - */ + + /++ + + Requests the passage in text format from the API and returns it. + + The (case-insensitive) name of the book being searched are + + contained in the argument book. The verse(s) being looked up are + + contained in the argument verses. + + + + Example: getPassage("John", "3:16-21") + +/ string getPassage(in char[] book, in char[] verse) in (bookValid(book), "Invalid book") @@ -258,7 +253,7 @@ class ESVApi { char[] params, response; - params = []; + params = []; { string[] parambuf; @@ -289,21 +284,20 @@ class ESVApi } response = makeRequest(format!"text/?q=%s+%s"( - book - .capitalize() - .tr(" ", "+"), - verse) ~ params ~ extraParameters); + book.capitalize().tr(" ", "+"), verse) + ~ params ~ extraParameters); + return response.parseJSON()["passages"][0].str; } - /** - * Requests an audio track of the verse(s) from the API and - * returns a file path containing an MP3 sound track. - * The (case-insensitive) name of the book being searched are - * contained in the argument book. The verse(s) being looked up are - * contained in the argument verses. - * - * Example: getAudioPassage("John", "3:16-21") - */ + /++ + + Requests an audio track of the verse(s) from the API and + + returns a file path containing an MP3 sound track. + + The (case-insensitive) name of the book being searched are + + contained in the argument book. The verse(s) being looked up are + + contained in the argument verses. + + + + Example: getAudioPassage("John", "3:16-21") + +/ string getAudioPassage(in char[] book, in char[] verse) in (bookValid(book), "Invalid book") @@ -313,28 +307,25 @@ class ESVApi tmpFile = File(_tmp, "w"); tmpFile.write(makeRequest(format!"audio/?q=%s+%s"( - book - .capitalize() - .tr(" ", "+"), - verse) - )); + book.capitalize().tr(" ", "+"), verse))); + return _tmp; } - /** - * Requests a passage search for the given query. - * Returns a string containing JSON data representing - * the results of the search. - * - * Example: search("It is finished") - */ + /++ + + Requests a passage search for the given query. + + Returns a string containing JSON data representing + + the results of the search. + + + + Example: search("It is finished") + +/ char[] search(in string query) { return makeRequest("search/?q=" ~ query.tr(" ", "+")); } - /** - * Calls search() and formats the results nicely as plain text. - */ + /++ + + Calls search() and formats the results nicely as plain text. + +/ char[] searchFormat(alias fmt = "\033[1m%s\033[0m\n %s\n") (in string query, int lineLength = 0) /* 0 means default */ @@ -385,52 +376,52 @@ class ESVApi } } -/** - * Structure containing parameters passed to the ESV API. - */ +/++ + + Structure containing parameters passed to the ESV API. + +/ struct ESVApiOptions { bool[string] b; int[string] i; ESVIndent indent_using; - /** - * If initialise is true, initialise an ESVApiOptions - * structure with the default values. - */ + /++ + + If initialise is true, initialise an ESVApiOptions + + structure with the default values. + +/ this(bool initialise) nothrow { if (!initialise) return; - b["include-passage-references"] = true; - b["include-verse-numbers"] = true; - b["include-first-verse-numbers"] = true; - b["include-footnotes"] = true; - b["include-footnote-body"] = true; - b["include-headings"] = true; - b["include-short-copyright"] = true; - b["include-copyright"] = false; + b["include-passage-references"] = true; + b["include-verse-numbers"] = true; + b["include-first-verse-numbers"] = true; + b["include-footnotes"] = true; + b["include-footnote-body"] = true; + b["include-headings"] = true; + b["include-short-copyright"] = true; + b["include-copyright"] = false; b["include-passage-horizontal-lines"] = false; b["include-heading-horizontal-lines"] = false; - b["include-selahs"] = true; - b["indent-poetry"] = true; - i["horizontal-line-length"] = 55; - i["indent-paragraphs"] = 2; - i["indent-poetry-lines"] = 4; - i["indent-declares"] = 40; - i["indent-psalm-doxology"] = 30; - i["line-length"] = 0; + b["include-selahs"] = true; + b["indent-poetry"] = true; + i["horizontal-line-length"] = 55; + i["indent-paragraphs"] = 2; + i["indent-poetry-lines"] = 4; + i["indent-declares"] = 40; + i["indent-psalm-doxology"] = 30; + i["line-length"] = 0; indent_using = ESVIndent.SPACE; } } -/** - * Exception thrown on API errors. - * - * Currently only used when there is no search results - * following a call of searchFormat. - */ +/++ + + Exception thrown on API errors. + + + + Currently only used when there is no search results + + following a call of searchFormat. + +/ class ESVException : Exception { mixin basicExceptionCtors; From 467573c99aefb3d646214166bfa19d9f23f8627a Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Mon, 26 Feb 2024 19:07:16 +1300 Subject: [PATCH 045/133] reword version --- esv.d | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esv.d b/esv.d index f9429ed..9d19d74 100644 --- a/esv.d +++ b/esv.d @@ -105,7 +105,7 @@ main(string[] args) } if (VFlag) { - writeln("esv version " ~ VERSION); + writeln("esv " ~ VERSION); return 0; } From 5a9103bfa8ecd730f5ae05131e6a80402b9cd1a2 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Wed, 28 Feb 2024 13:06:44 +1300 Subject: [PATCH 046/133] mark development version --- esv.d | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esv.d b/esv.d index 9d19d74..14b3df3 100644 --- a/esv.d +++ b/esv.d @@ -37,7 +37,7 @@ import esvapi; @safe: -enum VERSION = "0.2.0"; +enum VERSION = "0.2.0-dev"; bool aFlag; /* audio */ string CFlag; /* config */ From 4bb8d391fd33955cad3f99f41a27b49b7d0a470f Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Wed, 28 Feb 2024 15:48:56 +1300 Subject: [PATCH 047/133] initialise sourcehut CI esv is now built and briefly tested on Arch Linux and OpenBSD. --- .builds/archlinux.yml | 31 +++++++++++++++++++++++++++++++ .builds/openbsd.yml | 17 +++++++++++++++++ .editorconfig | 2 +- 3 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 .builds/archlinux.yml create mode 100644 .builds/openbsd.yml diff --git a/.builds/archlinux.yml b/.builds/archlinux.yml new file mode 100644 index 0000000..bcc71fb --- /dev/null +++ b/.builds/archlinux.yml @@ -0,0 +1,31 @@ +--- +image: archlinux +packages: + - bmake + - dmd + - curl + - ldc + - nix +sources: + - "https://git.sr.ht/~jeremy/esv" +tasks: + - prepare: | + printf 'experimental-features = nix-command flakes\n' \ + | sudo tee -a /etc/nix/nix.conf + sudo systemctl start nix-daemon + sudo usermod -aG nix-users build + - build-dmd: | + cd esv + ./configure -c dmd + bmake all clean + - build-ldc: | + cd esv + ./configure -c ldc2 + bmake + - install: | + sudo bmake -Cesv install + - test: | + # very basic test :) + esv Matthew 5-7 >/dev/null + - flake: | + nix build ./esv diff --git a/.builds/openbsd.yml b/.builds/openbsd.yml new file mode 100644 index 0000000..caeffc6 --- /dev/null +++ b/.builds/openbsd.yml @@ -0,0 +1,17 @@ +--- +image: openbsd/latest +packages: + - curl + - ldc +sources: + - "https://git.sr.ht/~jeremy/esv" +tasks: + - build: | + cd esv + ./configure + make + - install: | + doas make -Cesv install + - test: | + # very basic test :) + esv Matthew 5-7 >/dev/null diff --git a/.editorconfig b/.editorconfig index f3bedeb..7615472 100644 --- a/.editorconfig +++ b/.editorconfig @@ -5,6 +5,6 @@ end_of_line = lf indent_style = tab indent_size = 4 -[*.nix] +[*.{nix,yml}] indent_style = space indent_size = 2 \ No newline at end of file From f1b79c2a7ca2a1d7a9c9b9aca36b536bfcba44bd Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Wed, 28 Feb 2024 16:18:06 +1300 Subject: [PATCH 048/133] refactor configure script Generate a makefile config instead of a makefile with predefined constants. This makes the build a bit more easier to modify. Also improve the makefile and apply some suggestions from shellcheck. :) --- .gitignore | 2 +- Makefile | 29 +++++++++++++++++ configure | 95 +++++++++++++++++------------------------------------- 3 files changed, 60 insertions(+), 66 deletions(-) create mode 100644 Makefile diff --git a/.gitignore b/.gitignore index 04c1e2b..2564793 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,4 @@ esv result -Makefile +config.mk \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d4f9679 --- /dev/null +++ b/Makefile @@ -0,0 +1,29 @@ +IMPORT = import +PREFIX = /usr/local +MANPREFIX = ${PREFIX}/man + +DC = ${_DC} +CFLAGS = ${_CFLAGS} +OBJS = ${_OBJS} + +all: esv + +include config.mk + +esv: ${OBJS} + ${DC} ${_LDFLAGS} -of=$@ ${OBJS} + +.SUFFIXES: .d .o + +.d.o: + ${DC} ${CFLAGS} -c $< + +clean: + rm -f esv ${OBJS} ${INIOBJS} + +install: esv + install -Dm755 esv ${DESTDIR}${PREFIX}/bin/esv + install -Dm644 esv.1 ${DESTDIR}${MANPREFIX}/man1 + install -Dm644 esv.conf.5 ${DESTDIR}${MANPREFIX}/man5 + +.PHONY: all clean install diff --git a/configure b/configure index e3a908a..3f9e07c 100755 --- a/configure +++ b/configure @@ -4,40 +4,9 @@ set -e -import=import - -mkf=Makefile +mkf=config.mk objs='esv.o esvapi.o initial.o' srcs='esv.d esvapi.d initial.d' -makefile=' -IMPORT = '"$import"' -PREFIX = /usr/local -MANPREFIX = ${PREFIX}/man - -DC = ${_DC} -CFLAGS = ${_CFLAGS} -OBJS = ${_OBJS} - -all: esv - -esv: ${OBJS} - ${DC} ${_LDFLAGS} -of=$@ ${OBJS} - -.SUFFIXES: .d .o - -.d.o: - ${DC} ${CFLAGS} -c $< - -clean: - rm -f esv ${OBJS} ${INIOBJS} - -install: - install -Dm755 esv ${DESTDIR}${PREFIX}/bin/esv - install -Dm644 esv.1 ${DESTDIR}${MANPREFIX}/man1/esv.1 - install -Dm644 esv.conf.5 ${DESTDIR}${MANPREFIX}/man5/esv.conf.5 - -.PHONY: all clean install -' # utility functions @@ -45,10 +14,10 @@ present () { command -v "$1" 1>/dev/null 2>/dev/null } using () { - >&2 printf "using $1\n" + >&2 printf "using %s\n" "$1" } -error () { - >&2 printf "$(basename $0): $1\n" +throw () { + >&2 printf "%s: %s\n" "$(basename "$0")" "$1" exit 1 } @@ -56,7 +25,7 @@ error () { ## D compiler gen_DC () { - if ! [ -z "$dc" ]; then + if [ -n "$dc" ]; then using "$dc" return 0 fi @@ -65,7 +34,7 @@ gen_DC () { elif present dmd; then dc=dmd else - error "D compiler not found; install ldc or dmd" + throw "D compiler not found; install ldc or dmd" fi using "$dc" @@ -101,7 +70,7 @@ gen_LDFLAGS () { fi fi if [ -z "$debug" ]; then - if ! [ -z "$ldflags" ]; then ldflags="$ldflags "; fi + if [ -n "$ldflags" ]; then ldflags="$ldflags "; fi ldflags="$ldflags-L--gc-sections" fi @@ -118,7 +87,7 @@ while getopts c:dhr ch; do case "$OPTARG" in ldc2) dc="ldc2" ;; dmd) dc="dmd" ;; - *) error "unknown D compiler '$OPTARG' specified (valid options: ldc2, dmd)" ;; + *) throw "unknown D compiler '$OPTARG' specified (valid options: ldc2, dmd)" ;; esac ;; d) debug=1 ;; @@ -135,7 +104,7 @@ options: EOF exit 0 ;; - ?) exit 1 ;; + '?') exit 1 ;; :) exit 1 ;; esac done @@ -147,33 +116,29 @@ gen_CFLAGS gen_LDFLAGS rm -f "$mkf" -printf '# begin generated definitions' >>"$mkf" -printf ' + +{ + printf '# begin generated definitions _DC = %s _CFLAGS = %s _LDFLAGS = %s -' \ - "$dc" \ - "$cflags" \ - "$ldflags" \ - >>"$mkf" -## generate obj list -printf '_OBJS =' >>"$mkf" -for obj in $objs; do - printf " $obj" >>"$mkf" -done -printf '\n' >>"$mkf" -printf '# end generated definitions\n' >>"$mkf" +' "$dc" "$cflags" "$ldflags" -printf "$makefile" >>"$mkf" + ## generate obj list + printf '_OBJS =' + for obj in $objs; do + printf ' %s' "$obj" + done + printf '\n# end generated definitions\n' -## generate dependency list ->&2 printf "generating dependency list\n" -printf '\n# begin generated dependencies\n' >>"$mkf" -i=1 -for obj in $objs; do - "$dc" -O0 -o- -makedeps \ - "$(printf "$srcs" | awk '{print $'"$i"'}')" >>"$mkf" - i="$(($i + 1))" -done -printf '# end generated dependencies\n' >>"$mkf" + ## generate dependency list + printf '\n# begin generated dependencies\n' + i=1 + for obj in $objs; do + "$dc" -o- -makedeps \ + "$(printf '%s' "$srcs" | awk '{print $'"$i"'}')" + i="$((i + 1))" + done + unset i + printf '# end generated dependencies\n' +} >>"$mkf" From 024e612dabb23a6b5bc66383a6e955022f806151 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Wed, 28 Feb 2024 16:22:39 +1300 Subject: [PATCH 049/133] move to sourcehut --- README.md | 2 +- config.di | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 536af6a..f697707 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ to be run as the root user. First, download the source code: - $ git clone https://github.com/jtbx/esv + $ git clone https://git.sr.ht/~jeremy/esv $ cd esv Now, compile and install: diff --git a/config.di b/config.di index 8c14aab..eb0866c 100644 --- a/config.di +++ b/config.di @@ -11,6 +11,4 @@ enum DEFAULT_MPEGPLAYER = "mpg123"; enum ENV_CONFIG = "ESV_CONFIG"; enum ENV_PLAYER = "ESV_PLAYER"; -enum BUGREPORTURL = "https://codeberg.org/jtbx/esv/issues"; - -// vi: ft=d +enum BUGREPORTURL = "https://todo.sr.ht/~jeremy/esv"; From de08bf5c701f79e5f82c870b4b03bcb0d0817b04 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Thu, 29 Feb 2024 09:51:13 +1300 Subject: [PATCH 050/133] esv.1: revise manual Reword much of the manual, add the -s option, and add AUTHORS and BUGS sections among other small changes. --- esv.1 | 105 ++++++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 76 insertions(+), 29 deletions(-) diff --git a/esv.1 b/esv.1 index ee69fc9..666c199 100644 --- a/esv.1 +++ b/esv.1 @@ -1,37 +1,48 @@ -.Dd $Mdocdate: December 19 2023 $ +.Dd $Mdocdate: February 29 2024 $ .Dt ESV 1 .Os .Sh NAME .Nm esv -.Nd read the Bible from your terminal +.Nd read the Bible .Sh SYNOPSIS .Nm esv .Bk -words .Op Fl aFfHhNnRrV .Op Fl C Ar config .Op Fl l Ar length +.Op Fl s Ar query .Ar book verses .Ek .Sh DESCRIPTION .Nm -is a program that displays passages of the Bible on your terminal. -It can also play recorded audio tracks of certain passages, -through integration with the +displays Bible passages on your terminal. It can also play recorded +audio tracks of certain passages, through integration with an MP3 +player utility. +.Pp +See the section +.Sx EXAMPLES +below for some basic usage examples. Verses can be provided in the +format of +.Em chapter , +.Em chapter-chapter , +.Em chapter:verse , +and +.Em chapter:verse-verse . +.Pp +By default, .Xr mpg123 1 -utility. While audio is playing, you can use the standard mpg123 -controls: spacebar to pause/resume, comma to rewind, period -to fast-forward, etc. Read about the -.Fl C -option in mpg123's manual for more information. +is used as the MP3 player. However, this can be overridden; see the +.Sx ENVIRONMENT +section for more information on this. .Pp The options are as follows: -.Bl -tag -width keyword +.Bl -tag -width 123456 .It Fl a -Instead of displaying text passages, play a recorded audio track. -.It Fl C Ar config -Use -.Ar config -as the configuration file path. This overrides the +Play a recorded audio track rather than showing a passage. +.It Fl C Ar configfile +Read the configuration from the path +.Ar configfile . +This overrides the .Ev ESV_CONFIG environment variable (see section .Sx ENVIRONMENT ) . @@ -44,9 +55,11 @@ Exclude headings. .It Fl h Include headings (the default). .It Fl l Ar length -Use +Limit the width of text passage lines at .Ar length -as the maximum line length. +characters. If +.Ar length +is 0, do not limit them at all. .It Fl N Exclude verse numbers. .It Fl n @@ -55,33 +68,67 @@ Include verse numbers (the default). Exclude passage references. .It Fl r Include passage references (the default). +.It Fl s Ar query +Rather than displaying a passage or playing an audio track, search the +Bible for instances of +.Ar query . .It Fl V Print the version number and exit. +.El +.Pp +The options +.Fl FfHhlNnRr +only apply when reading text passages, that is, when +.Fl a +or +.Fl s +is not given. .Sh ENVIRONMENT .Bl -tag -width ESV_CONFIG .It Ev ESV_CONFIG -Where to read the configuration file, rather than using the default location (see section +Where to read the configuration file, rather than using the default +location (see section .Sx FILES ) . .It Ev ESV_PLAYER -What MP3 player to use for playing audio, rather than using mpg123. -Using mpg123 is recommended over other players such as mpv, because -mpv's controls don't work well when started by another process -for some reason. +The name of the MP3 player to use for playing audio passages. If this +is not set, +.Nm +will look for +.Xr mpg123 1 +and start it. .Sh FILES .Bl -tag -width ~/.config/esv.conf .It Pa ~/.config/esv.conf default configuration file location .El - .Sh EXAMPLES -Read Psalm 23: +Read Matthew 6:24: .Pp -.Dl esv Psalm 23 +.Dl esv Matthew 6:24 .Pp -Listen to a recorded audio track of Matthew 5-7: +Listen to a recorded audio track of Psalm 23: .Pp -.Dl esv -a Matthew 5-7 +.Dl esv -a Psalm 23 +.Pp +Search the Bible for instances of the word +.Dq dogs : +.Pp +.Dl esv -s dogs .Pp - .Sh SEE ALSO .Xr esv.conf 5 +.Sh AUTHORS +.An Jeremy Baxter Aq Mt jtbx@disroot.org +.Sh BUGS +Currently searching the Bible using +.Fl s +only shows a portion of the results. If you have many results for your +query, and there are not many in the New Testament compared to those +in the Old Testament, your search may have been affected by this bug. +.Pp +If you have discovered a bug in +.Nm , +please report it to the bug tracker found at +.Lk https://todo.sr.ht/~jeremy/esv +or send an email to +.Mt ~jeremy/esv@todo.sr.ht . From 11543da52f2914e3ae4808a052cc49bd07ad2890 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Fri, 8 Mar 2024 11:26:36 +1300 Subject: [PATCH 051/133] esv: handle absence of search results with -s previously this would throw an unhandled ESVException. --- esv.d | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/esv.d b/esv.d index 14b3df3..f4c0a75 100644 --- a/esv.d +++ b/esv.d @@ -198,7 +198,10 @@ key = %s } if (sFlag) { - writeln(esv.searchFormat(sFlag)); + try + writeln(esv.searchFormat(sFlag)); + catch (ESVException) + die("no results for search"); return 0; } From e713ff9ddf363917471199141d2cd1c3ddd1defb Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Thu, 28 Mar 2024 09:29:14 +1300 Subject: [PATCH 052/133] makefile: fix man page installation --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index d4f9679..3327852 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ IMPORT = import PREFIX = /usr/local -MANPREFIX = ${PREFIX}/man +MANPREFIX = ${PREFIX}/share/man DC = ${_DC} CFLAGS = ${_CFLAGS} From 0b3626b36c6d2f049ace2f4cbbce5172e1f439c3 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Tue, 18 Jun 2024 16:43:33 +1200 Subject: [PATCH 053/133] esvapi: expose CurlException --- esvapi.d | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esvapi.d b/esvapi.d index 6b09644..abf3e89 100644 --- a/esvapi.d +++ b/esvapi.d @@ -30,6 +30,8 @@ import std.stdio : File; import std.string : capitalize, tr, wrap; import std.net.curl : HTTP; +public import std.net.curl : CurlException; + @safe: /++ Indentation style to use when formatting passages. +/ From 0c0a5cab709494495b801e9f25bc7356f1ca4726 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Thu, 28 Mar 2024 10:38:09 +1300 Subject: [PATCH 054/133] esv: handle thrown CurlExceptions --- esv.d | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/esv.d b/esv.d index f4c0a75..db7a406 100644 --- a/esv.d +++ b/esv.d @@ -175,7 +175,10 @@ key = %s if (aFlag) { string tmpf, mpegPlayer; - tmpf = esv.getAudioPassage(args[1], args[2]); + try + tmpf = esv.getAudioPassage(args[1], args[2]); + catch (CurlException e) + die(e.msg); mpegPlayer = environment.get(ENV_PLAYER, DEFAULT_MPEGPLAYER); /* check for an audio player */ @@ -202,6 +205,8 @@ key = %s writeln(esv.searchFormat(sFlag)); catch (ESVException) die("no results for search"); + catch (CurlException e) + die(e.msg); return 0; } @@ -237,7 +242,10 @@ key = %s if (RFlag) esv.opts.b["include-passage-references"] = false; if (lFlag != 0) esv.opts.i["line-length"] = lFlag; - writeln(esv.getPassage(args[1].parseBook(), args[2])); + try + writeln(esv.getPassage(args[1].parseBook(), args[2])); + catch (CurlException e) + die(e.msg); return 0; } From a1d6a3e84cd09bbbd741fe5404ef3c4b622a4a0a Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Tue, 2 Apr 2024 14:57:15 +1300 Subject: [PATCH 055/133] esv.1: add unmatched .El --- esv.1 | 1 + 1 file changed, 1 insertion(+) diff --git a/esv.1 b/esv.1 index 666c199..f0d8e81 100644 --- a/esv.1 +++ b/esv.1 @@ -96,6 +96,7 @@ is not set, will look for .Xr mpg123 1 and start it. +.El .Sh FILES .Bl -tag -width ~/.config/esv.conf .It Pa ~/.config/esv.conf From b662605f08d72a9aebcff4c91ab5002598346595 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Thu, 2 May 2024 09:25:04 +1200 Subject: [PATCH 056/133] editorconfig: add .el settings --- .editorconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.editorconfig b/.editorconfig index 7615472..90cbacb 100644 --- a/.editorconfig +++ b/.editorconfig @@ -5,6 +5,6 @@ end_of_line = lf indent_style = tab indent_size = 4 -[*.{nix,yml}] +[*.{el,nix,yml}] indent_style = space indent_size = 2 \ No newline at end of file From 00e6e3d05d0fc485a08196060ec05a34823486f3 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Wed, 1 May 2024 11:20:52 +1200 Subject: [PATCH 057/133] esv.el: add a package for integrating into Emacs --- esv.el | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 esv.el diff --git a/esv.el b/esv.el new file mode 100644 index 0000000..ff3182a --- /dev/null +++ b/esv.el @@ -0,0 +1,53 @@ +;;; esv.el --- read the Bible from Emacs -*- lexical-binding:t -*- + +(defgroup esv nil + "Read the Bible." + :prefix "esv-" + :group 'applications) + +(defcustom esv-columns 72 + "Length of each line output by `esv'." + :type 'natnum + :group 'esv) +(defcustom esv-mode-hook nil + "Hook run after entering `esv-mode'." + :type 'hook + :group 'esv) +(defcustom esv-process "esv" + "Name of the process created by `esv'." + :type 'string + :group 'esv) +(defcustom esv-program "esv" + "Path to or name of the program started by `esv'." + :type 'string + :group 'esv) + +(define-derived-mode esv-mode text-mode "ESV-Bible" + "Major mode used for reading the Bible with `esv'." + :group 'esv + + (read-only-mode)) + +(defun esv (book verses) + "Fetch the Bible passage identified by BOOK and VERSES. +The result will be redirected to a buffer specified by `esv-buffer'." + (interactive "MBook: \nMVerses: ") + (let ((buffer (concat book " " verses))) + (catch 'buffer-exists + (when (get-buffer buffer) + (message "Buffer `%s' already exists" buffer) + (throw 'buffer-exists nil)) + ;; execute esv + (call-process esv-program nil buffer t + ;; arguments + (format "-l%d" esv-columns) book verses) + ;; display buffer in another window + (display-buffer buffer) + ;; move point to the beginning of the buffer + (with-current-buffer buffer + (esv-mode) + (goto-char (point-min))) + (set-window-start (get-buffer-window buffer) (point-min)) + t))) + +(provide 'esv) From 82a9eb84f0aa2a95096ae9fb0715e446200a4e10 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Fri, 3 May 2024 20:23:58 +1200 Subject: [PATCH 058/133] esvapi: fix documentation for getPassage() --- esvapi.d | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esvapi.d b/esvapi.d index abf3e89..9a0016a 100644 --- a/esvapi.d +++ b/esvapi.d @@ -242,8 +242,8 @@ class ESVApi /++ + Requests the passage in text format from the API and returns it. - + The (case-insensitive) name of the book being searched are - + contained in the argument book. The verse(s) being looked up are + + The (case-insensitive) name of the book being searched is + + contained in the argument book. The verses being looked up are + contained in the argument verses. + + Example: getPassage("John", "3:16-21") From 47b09314f7ae3c3f0f8ec0df677d758b92e31a15 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Mon, 17 Jun 2024 17:56:20 +1200 Subject: [PATCH 059/133] esv.1: use semantic newlines See the section "Use semantic newlines" of man-pages(7) for some reasoning on this. --- esv.1 | 37 +++++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/esv.1 b/esv.1 index f0d8e81..864cb0e 100644 --- a/esv.1 +++ b/esv.1 @@ -15,14 +15,14 @@ .Ek .Sh DESCRIPTION .Nm -displays Bible passages on your terminal. It can also play recorded -audio tracks of certain passages, through integration with an MP3 -player utility. +displays Bible passages on your terminal. +It can also play recorded audio tracks of certain passages, +through integration with an MP3 player utility. .Pp See the section .Sx EXAMPLES -below for some basic usage examples. Verses can be provided in the -format of +below for some basic usage examples. +Verses can be provided in the format of .Em chapter , .Em chapter-chapter , .Em chapter:verse , @@ -31,7 +31,9 @@ and .Pp By default, .Xr mpg123 1 -is used as the MP3 player. However, this can be overridden; see the +is used as the MP3 player. +However, this can be overridden; +see the .Sx ENVIRONMENT section for more information on this. .Pp @@ -69,8 +71,8 @@ Exclude passage references. .It Fl r Include passage references (the default). .It Fl s Ar query -Rather than displaying a passage or playing an audio track, search the -Bible for instances of +Rather than displaying a passage or playing an audio track, +search the Bible for instances of .Ar query . .It Fl V Print the version number and exit. @@ -78,7 +80,8 @@ Print the version number and exit. .Pp The options .Fl FfHhlNnRr -only apply when reading text passages, that is, when +only apply when reading text passages, +that is, when .Fl a or .Fl s @@ -86,12 +89,12 @@ is not given. .Sh ENVIRONMENT .Bl -tag -width ESV_CONFIG .It Ev ESV_CONFIG -Where to read the configuration file, rather than using the default -location (see section +Where to read the configuration file, +rather than using the default location (see section .Sx FILES ) . .It Ev ESV_PLAYER -The name of the MP3 player to use for playing audio passages. If this -is not set, +The name of the MP3 player to use for playing audio passages. +If this is not set, .Nm will look for .Xr mpg123 1 @@ -123,9 +126,11 @@ Search the Bible for instances of the word .Sh BUGS Currently searching the Bible using .Fl s -only shows a portion of the results. If you have many results for your -query, and there are not many in the New Testament compared to those -in the Old Testament, your search may have been affected by this bug. +only shows a portion of the results. +If you have many results for your query, +and there are not many in the New Testament compared to those +in the Old Testament, +your search may have been affected by this bug. .Pp If you have discovered a bug in .Nm , From ed6813f2ef15b54c6737f0d0ebc7b23e94610281 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Mon, 17 Jun 2024 18:01:01 +1200 Subject: [PATCH 060/133] esv.1: add documentation for underscores in book names --- esv.1 | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/esv.1 b/esv.1 index 864cb0e..4dd81b5 100644 --- a/esv.1 +++ b/esv.1 @@ -28,6 +28,16 @@ Verses can be provided in the format of .Em chapter:verse , and .Em chapter:verse-verse . +If the name of your desired book has a space in it, e.g. +.Dq "1 Corinthians" , +you can put an underscore in the place of the space, +or you can just pass the full book name with the space +by surrounding the argument with quotes in your shell. +Both +.Dq 1_Corinthians +and +.Dq "1 Corinthians" +are valid book names. .Pp By default, .Xr mpg123 1 From a3a575e8c5e26caa5c1466f5ed2d3d2014ed919d Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Tue, 18 Jun 2024 13:06:46 +1200 Subject: [PATCH 061/133] esv, esvapi: include 2023 in copyright year range --- esv.d | 2 +- esvapi.d | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esv.d b/esv.d index db7a406..c195354 100644 --- a/esv.d +++ b/esv.d @@ -2,7 +2,7 @@ * esv: read the Bible from your terminal * * The GPLv2 License (GPLv2) - * Copyright (c) 2024 Jeremy Baxter + * 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 diff --git a/esvapi.d b/esvapi.d index 9a0016a..a7a021e 100644 --- a/esvapi.d +++ b/esvapi.d @@ -2,7 +2,7 @@ * esvapi.d: a reusable interface to the ESV HTTP API * * The GPLv2 License (GPLv2) - * Copyright (c) 2024 Jeremy Baxter + * 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 From 82ce0f86d7276aa7ee0ea8fab53e59a6029ca044 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Tue, 18 Jun 2024 13:07:00 +1200 Subject: [PATCH 062/133] esv.1: bump Mdocdate --- esv.1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esv.1 b/esv.1 index 4dd81b5..64acc1c 100644 --- a/esv.1 +++ b/esv.1 @@ -1,4 +1,4 @@ -.Dd $Mdocdate: February 29 2024 $ +.Dd $Mdocdate: June 17 2024 $ .Dt ESV 1 .Os .Sh NAME From 6bd5af01f83d46f42bcb4ec30cd898c8d01a97d8 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Tue, 18 Jun 2024 16:24:34 +1200 Subject: [PATCH 063/133] esv: trim configuration file --- config.di | 2 -- esv.d | 5 +---- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/config.di b/config.di index eb0866c..0c1473e 100644 --- a/config.di +++ b/config.di @@ -1,5 +1,3 @@ -/* default configuration for esv */ - module config; public: diff --git a/esv.d b/esv.d index c195354..c9ec08d 100644 --- a/esv.d +++ b/esv.d @@ -140,14 +140,11 @@ config: if (!configPath.exists()) { mkdirRecurse(configPath.dirName()); configPath.write(format! -"# Default esv configuration file. +"## Configuration file for esv. # An API key is required to access the ESV Bible API. [api] key = %s -# If you really need to, you can specify -# custom API parameters using `parameters`: -#parameters = &my-parameter=value # Settings that modify how passages are displayed: [passage] From d5cacb040101422e2c24b39e9c56b89e7271676c Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Tue, 18 Jun 2024 17:48:37 +1200 Subject: [PATCH 064/133] initial: re-vendor @ c00e0fa c00e0fa avoid converting objects twice baafe10 fix doc comment --- initial.d | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/initial.d b/initial.d index 7f4a585..289c33c 100644 --- a/initial.d +++ b/initial.d @@ -218,7 +218,7 @@ struct INISection keys = new string[string]; } - /+ + /++ + Returns the value of `k` in this section or + `defaultValue` if the key is not present. +/ @@ -234,10 +234,10 @@ struct INISection + with a message containing location information. +/ T - keyAs(T)(string k, string defaultValue = null) + keyAs(T)(string k, T defaultValue) { try - return key(k, defaultValue).to!T(); + return k in keys ? keys[k].to!T() : defaultValue; catch (ConvException) throw new INITypeException( format!"unable to convert [%s].%s to %s"(name, k, T.stringof)); From d68b83b722557ae9bbf0a05d31e69ed1168d0774 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Tue, 18 Jun 2024 18:12:57 +1200 Subject: [PATCH 065/133] esv: preserve compatibility with the newest initial --- esv.d | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esv.d b/esv.d index c9ec08d..5caf3d8 100644 --- a/esv.d +++ b/esv.d @@ -218,14 +218,14 @@ key = %s ]) { try esv.opts.b["include-" ~ key] = - ini["passage"].keyAs!bool(key, "true"); + ini["passage"].keyAs!bool(key, true); catch (INITypeException e) die(configPath ~ ": " ~ e.msg); } /* Get line-length ([passage]) */ try esv.opts.i["line-length"] = - ini["passage"].keyAs!int("line-length", "0"); + ini["passage"].keyAs!int("line-length", 0); catch (INITypeException e) die(configPath ~ ": " ~ e.msg); From 982273b9d71375806bcb2049ae2bf48e24caed5b Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Tue, 18 Jun 2024 18:41:30 +1200 Subject: [PATCH 066/133] esv: detect passage line length based on terminal width Implements: https://todo.sr.ht/~jeremy/esv/2 --- esv.d | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/esv.d b/esv.d index 5caf3d8..dfa7dc5 100644 --- a/esv.d +++ b/esv.d @@ -44,6 +44,7 @@ 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 */ @@ -70,7 +71,7 @@ main(string[] args) import core.sys.openbsd.unistd : pledge; import std.string : toStringz; - promises = toStringz("stdio rpath wpath cpath inet dns proc exec prot_exec"); + promises = toStringz("stdio rpath wpath cpath inet dns tty proc exec prot_exec"); pledge(promises, null); }(); @@ -87,7 +88,7 @@ main(string[] args) "C", &CFlag, "F", &FFlag, "f", &fFlag, "H", &HFlag, "h", &hFlag, - "l", &lFlag, + "l", &onLineLength, "N", &NFlag, "n", &nFlag, "R", &RFlag, "r", &rFlag, "s", &sFlag, @@ -100,8 +101,6 @@ main(string[] args) "missing argument for option " ~ e.extractOpt()); die(e.msg); /* catch-all */ - } catch (ConvException e) { - die("illegal argument to -l option -- must be integer"); } if (VFlag) { @@ -225,7 +224,8 @@ key = %s /* Get line-length ([passage]) */ try esv.opts.i["line-length"] = - ini["passage"].keyAs!int("line-length", 0); + lFlagSpecified ? lFlag : + ini["passage"].keyAs!int("line-length", terminalColumns()); catch (INITypeException e) die(configPath ~ ": " ~ e.msg); @@ -237,7 +237,6 @@ key = %s 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; - if (lFlag != 0) esv.opts.i["line-length"] = lFlag; try writeln(esv.getPassage(args[1].parseBook(), args[2])); @@ -263,6 +262,17 @@ die(string mesg) @trusted 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) { @@ -280,6 +290,16 @@ extractOpt(in GetOptException e) 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) { From b5399bc8fa63b1fbed69850da98dc5eb8fad97d6 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Tue, 18 Jun 2024 19:24:59 +1200 Subject: [PATCH 067/133] esv.1: document passage line length detection --- esv.1 | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/esv.1 b/esv.1 index 64acc1c..6798f2a 100644 --- a/esv.1 +++ b/esv.1 @@ -1,4 +1,4 @@ -.Dd $Mdocdate: June 17 2024 $ +.Dd $Mdocdate: June 18 2024 $ .Dt ESV 1 .Os .Sh NAME @@ -67,11 +67,27 @@ Exclude headings. .It Fl h Include headings (the default). .It Fl l Ar length -Limit the width of text passage lines at +Limit the length of lines in passages at .Ar length characters. If .Ar length -is 0, do not limit them at all. +is 0, do not set a limit on line length. +.Pp +If this option is not given, +the line length limit will fall back on the value provided in +the configuration file; +see +.Xr esv.conf 5 . +.Pp +If there is no value present in the configuration file, +.Nm +will get the width of the terminal, +and will use this as the line length limit, +unless the terminal's width is greater than 78, +in which case the line length limit will be set to 78. +This heuristic exists for readability purposes, +and you can always override it by specifying your preference in +.Xr esv.conf 5 . .It Fl N Exclude verse numbers. .It Fl n From 98b3a385ad380bc5284802eb1003bfbbc59097d7 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Tue, 18 Jun 2024 19:30:14 +1200 Subject: [PATCH 068/133] esv.conf.5: revise and update This manual has been neglected and hasn't been touched since the initial source code release. Ouch! --- esv.conf.5 | 47 +++++++++++++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/esv.conf.5 b/esv.conf.5 index ad98cdc..34164c3 100644 --- a/esv.conf.5 +++ b/esv.conf.5 @@ -7,14 +7,15 @@ .Sh DESCRIPTION The .Xr esv 1 -utility uses a configuration file to customize its behaviour. -This file uses the standard Unix configuration file format, with -section-based key-value pairs. An example is listed below: +program uses a configuration file to customize its behaviour. +This file uses a basic plain text format, +with section-based key-value pairs. +An example is listed below: .Pp .Dl [section] .Dl key = value .Pp -Comments can be used by putting a pound +Comments can be used by putting a hashtag .Dq # symbol at the beginning of a line. .Pp @@ -26,18 +27,29 @@ The section contains settings that modify the way passages are displayed. .Bl -tag -width keyword .It Em footnotes -Boolean value that determines whether or not footnotes are displayed -under the text. +True/false value that determines whether +footnotes are displayed under the text. .It Em headings -Boolean value that determines whether or not headings are displayed. -.It Em passage_references -Boolean value that determines whether or not passage references are -displayed before the text. -.It Em verse_numbers -Boolean value that determines whether or not verse numbers are displayed. -.It Em line_length -Integer value that determines the maximum length for each line of -the passage. +True/false value that determines whether headings are displayed. +.It Em passage-references +True/false value that determines whether +passage references are displayed before the text. +.It Em verse-numbers +True/false value that determines whether verse numbers are displayed +in the text. +.It Em line-length +Integer value that determines the line length limit. +This is related to esv's +.Fl l +option; +it can be specified as 0 for unlimited line lengths, +or it can be left out entirely to choose an appropriate value +based on your terminal's width. +See the documentation for +.Fl l +in +.Xr esv 1 +for a thorough description of this heuristic. .El .It Sy [api] The @@ -49,19 +61,18 @@ passed to the ESV Bible API. Your API key, available from .Lk http://api.esv.org .Pp -This key is required, and is automatically filled in. +This key is required, +and is automatically filled in since esv 0.2.0. .It Em parameters Optional HTTP parameters passed to the API. If you are using this, make sure it starts with an ampersand symbol .Dq & . .El .El - .Sh FILES .Bl -tag -width ~/.config/esv.conf .It Pa ~/.config/esv.conf default configuration file location .El - .Sh SEE ALSO .Xr esv 1 From 81127066a0c52f96e699dfb0411a663d725a07f3 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Tue, 18 Jun 2024 19:35:34 +1200 Subject: [PATCH 069/133] README.md: update --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f697707..de64cd8 100644 --- a/README.md +++ b/README.md @@ -43,13 +43,14 @@ Commands prefixed with a dollar sign ($) are intended to be run as a standard user, and commands prefixed with a hash sign (#) are intended to be run as the root user. -First, download the source code: +In [the list of releases][1] select the latest release, and download the +archive with the name esv-X.X.X.tar.gz. After that extract it: - $ git clone https://git.sr.ht/~jeremy/esv - $ cd esv + $ tar -xf esv-*.tar.gz Now, compile and install: + $ cd esv-* $ ./configure $ make # make install @@ -68,4 +69,4 @@ License version 2. Full licenses are contained in the COPYING file. -Copyright (c) 2024 Jeremy Baxter +Copyright (c) 2023-2024 Jeremy Baxter From 387c397a5a5f6fbb187f428bee422a8c51008867 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Tue, 18 Jun 2024 19:54:41 +1200 Subject: [PATCH 070/133] README.md: fix broken link --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index de64cd8..ca8f51b 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,8 @@ Now, compile and install: $ make # make install +[1]: https://git.sr.ht/~jeremy/esv/refs + ## Documentation All documentation is contained in the manual pages. To access them, From 4c4ed78952ab60ebc3d8fe7ea6dff71ff4a2826f Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Wed, 19 Jun 2024 11:31:04 +1200 Subject: [PATCH 071/133] makefile: fix man page installation --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 3327852..25e82a6 100644 --- a/Makefile +++ b/Makefile @@ -23,7 +23,7 @@ clean: install: esv install -Dm755 esv ${DESTDIR}${PREFIX}/bin/esv - install -Dm644 esv.1 ${DESTDIR}${MANPREFIX}/man1 - install -Dm644 esv.conf.5 ${DESTDIR}${MANPREFIX}/man5 + install -Dm644 esv.1 ${DESTDIR}${MANPREFIX}/man1/esv.1 + install -Dm644 esv.conf.5 ${DESTDIR}${MANPREFIX}/man5/esv.conf.5 .PHONY: all clean install From ceaa1932109e57c157e42d5fa98040e5079f8e4a Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Wed, 19 Jun 2024 11:38:57 +1200 Subject: [PATCH 072/133] esv.1: update author address --- esv.1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esv.1 b/esv.1 index 6798f2a..0f91deb 100644 --- a/esv.1 +++ b/esv.1 @@ -148,7 +148,7 @@ Search the Bible for instances of the word .Sh SEE ALSO .Xr esv.conf 5 .Sh AUTHORS -.An Jeremy Baxter Aq Mt jtbx@disroot.org +.An Jeremy Baxter Aq Mt jeremy@baxters.nz .Sh BUGS Currently searching the Bible using .Fl s From 4bcda73b7de82d7b0f0346ca68cd30639746be4c Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Wed, 19 Jun 2024 11:39:06 +1200 Subject: [PATCH 073/133] esv.1: update bug reporting section --- esv.1 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/esv.1 b/esv.1 index 0f91deb..679458e 100644 --- a/esv.1 +++ b/esv.1 @@ -158,9 +158,9 @@ and there are not many in the New Testament compared to those in the Old Testament, your search may have been affected by this bug. .Pp -If you have discovered a bug in +If you have found a potential bug in .Nm , -please report it to the bug tracker found at +please report it to me using my email address above. +.Pp +Existing bugs can be found on the bug tracker: .Lk https://todo.sr.ht/~jeremy/esv -or send an email to -.Mt ~jeremy/esv@todo.sr.ht . From 3391c982b4acd96f3ad362f1558db03238537ee3 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Wed, 19 Jun 2024 11:44:50 +1200 Subject: [PATCH 074/133] esv.1: revise examples --- esv.1 | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/esv.1 b/esv.1 index 679458e..1a53e62 100644 --- a/esv.1 +++ b/esv.1 @@ -132,18 +132,18 @@ and start it. default configuration file location .El .Sh EXAMPLES -Read Matthew 6:24: +Read John 1:29-31: .Pp -.Dl esv Matthew 6:24 +.Dl esv John 1:29-31 .Pp -Listen to a recorded audio track of Psalm 23: +Listen to a recorded audio track of Psalm 128: .Pp -.Dl esv -a Psalm 23 +.Dl esv -a Psalm 128 .Pp -Search the Bible for instances of the word -.Dq dogs : +Search the Bible for the phrase +.Dq "in love" : .Pp -.Dl esv -s dogs +.Dl esv -s 'in love' .Pp .Sh SEE ALSO .Xr esv.conf 5 From 29dd1304f60ab3967631b37de69e3a6ff41aaa03 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Wed, 19 Jun 2024 12:27:16 +1200 Subject: [PATCH 075/133] esvapi: add empty lines between methods --- esvapi.d | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/esvapi.d b/esvapi.d index a7a021e..3dc4f93 100644 --- a/esvapi.d +++ b/esvapi.d @@ -218,18 +218,21 @@ class ESVApi final @property string key() const nothrow pure @nogc => _key; + /++ + Returns the subdirectory used to store temporary audio passages. +/ final @property string tmpDir() const nothrow pure @nogc => _tmp; + /++ + Returns the API URL currently in use. +/ final @property string url() const nothrow pure @nogc => _url; + /++ + Sets the API URL currently in use to the given url argument. +/ @@ -291,6 +294,7 @@ class ESVApi return response.parseJSON()["passages"][0].str; } + /++ + Requests an audio track of the verse(s) from the API and + returns a file path containing an MP3 sound track. @@ -313,6 +317,7 @@ class ESVApi return _tmp; } + /++ + Requests a passage search for the given query. + Returns a string containing JSON data representing @@ -325,6 +330,7 @@ class ESVApi { return makeRequest("search/?q=" ~ query.tr(" ", "+")); } + /++ + Calls search() and formats the results nicely as plain text. +/ From 08187f8ef72b74a33a271cb745466f95280aa2f1 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Wed, 19 Jun 2024 16:38:45 +1200 Subject: [PATCH 076/133] README.md: add sourcehut builds badge --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index ca8f51b..a785007 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ ## esv - read the Bible from your terminal +[![builds.sr.ht status](https://builds.sr.ht/~jeremy/esv.svg)](https://builds.sr.ht/~jeremy/esv) + `esv` displays passages of the English Standard Bible on your terminal. It connects to the ESV web API to retrieve the passages, and allows configuration through command-line options and the From cdabb25f747b1f229c7f6f6a3244b42f3cd7091b Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Thu, 20 Jun 2024 18:30:17 +1200 Subject: [PATCH 077/133] esvapi: add empty line --- esvapi.d | 1 + 1 file changed, 1 insertion(+) diff --git a/esvapi.d b/esvapi.d index 3dc4f93..f63a97e 100644 --- a/esvapi.d +++ b/esvapi.d @@ -153,6 +153,7 @@ bookValid(in char[] book) nothrow } return false; } + /++ + Returns true if the argument verse is a valid verse format. + Otherwise, returns false. From 77c2c4825a29961b03655b2b86f1c133e39553df Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Tue, 25 Jun 2024 09:40:53 +1200 Subject: [PATCH 078/133] esvapi: fetch all pages of search results Fixes: https://todo.sr.ht/~jeremy/esv/1 --- esv.1 | 17 +++++------------ esvapi.d | 41 +++++++++++++++++++++++++++++++++++------ 2 files changed, 40 insertions(+), 18 deletions(-) diff --git a/esv.1 b/esv.1 index 1a53e62..226aefd 100644 --- a/esv.1 +++ b/esv.1 @@ -1,4 +1,4 @@ -.Dd $Mdocdate: June 18 2024 $ +.Dd $Mdocdate: June 25 2024 $ .Dt ESV 1 .Os .Sh NAME @@ -150,17 +150,10 @@ Search the Bible for the phrase .Sh AUTHORS .An Jeremy Baxter Aq Mt jeremy@baxters.nz .Sh BUGS -Currently searching the Bible using -.Fl s -only shows a portion of the results. -If you have many results for your query, -and there are not many in the New Testament compared to those -in the Old Testament, -your search may have been affected by this bug. -.Pp -If you have found a potential bug in -.Nm , +Currently there are no known bugs in +.Nm ; +but if you think you've found a potential bug, please report it to me using my email address above. .Pp -Existing bugs can be found on the bug tracker: +Existing bugs and planned features can be found on the bug tracker: .Lk https://todo.sr.ht/~jeremy/esv diff --git a/esvapi.d b/esvapi.d index f63a97e..0356120 100644 --- a/esvapi.d +++ b/esvapi.d @@ -326,16 +326,45 @@ class ESVApi + + Example: search("It is finished") +/ - char[] - search(in string query) + string + search(in string query) @trusted { - return makeRequest("search/?q=" ~ query.tr(" ", "+")); + JSONValue[] pages, results; + JSONValue result; + + JSONValue + makeQuery(long page) + => parseJSON(makeRequest("search/?page-size=100" + ~ "&page=" ~ page.to!string() + ~ "&q=" ~ query.tr(" ", "+"))); + + pages ~= makeQuery(1); + if (pages[0]["total_pages"].integer == 1) { + result = JSONValue([ + "results": pages[0]["results"], + "total": JSONValue(pages[0]["results"].array.length) + ]); + return result.toString(); + } + + foreach (long i; 2 .. pages[0]["total_pages"].integer + 1) { + pages ~= makeQuery(i); + } + foreach (JSONValue page; pages) { + results ~= page["results"].array; + } + + result = JSONValue([ + "results": JSONValue(results), + "total": JSONValue(results.length) + ]); + return result.toString(); } /++ + Calls search() and formats the results nicely as plain text. +/ - char[] + string searchFormat(alias fmt = "\033[1m%s\033[0m\n %s\n") (in string query, int lineLength = 0) /* 0 means default */ { @@ -345,7 +374,7 @@ class ESVApi resp = parseJSON(search(query)); layout = []; - enforce!ESVException(resp["total_results"].integer != 0, + enforce!ESVException(resp["total"].integer != 0, "No results for search"); lineLength = lineLength == 0 ? 80 : lineLength; @@ -360,7 +389,7 @@ class ESVApi } }(); - return layout; + return layout.idup(); } protected char[] From 59c92c8db7c50b52aa9ad6034b0b59d86f535941 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Tue, 25 Jun 2024 15:27:26 +1200 Subject: [PATCH 079/133] esv: refactor compile time configuration --- config.di | 12 ++++++------ esv.d | 20 +++++++++----------- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/config.di b/config.di index 0c1473e..041e561 100644 --- a/config.di +++ b/config.di @@ -2,11 +2,11 @@ module config; public: -enum DEFAULT_APIKEY = "abfb7456fa52ec4292c79e435890cfa3df14dc2b"; -enum DEFAULT_CONFIGPATH = "~/.config/esv.conf"; -enum DEFAULT_MPEGPLAYER = "mpg123"; +enum esvVersion = "0.2.0-dev"; -enum ENV_CONFIG = "ESV_CONFIG"; -enum ENV_PLAYER = "ESV_PLAYER"; +enum apiKey = "abfb7456fa52ec4292c79e435890cfa3df14dc2b"; +enum configPath = "~/.config/esv.conf"; +enum mp3Player = "mpg123"; -enum BUGREPORTURL = "https://todo.sr.ht/~jeremy/esv"; +enum configEnv = "ESV_CONFIG"; +enum playerEnv = "ESV_PLAYER"; diff --git a/esv.d b/esv.d index dfa7dc5..e6ed9c4 100644 --- a/esv.d +++ b/esv.d @@ -30,15 +30,13 @@ import std.process : environment, executeShell; import std.stdio : writef, writeln, writefln, File; import std.string : splitLines; +import esvapi; import initial; -import config; -import esvapi; +import cf = config; @safe: -enum VERSION = "0.2.0-dev"; - bool aFlag; /* audio */ string CFlag; /* config */ bool fFlag, FFlag; /* footnotes */ @@ -80,10 +78,10 @@ main(string[] args) /* Parse command-line options */ try { - import std.getopt : cfg = config; + import std.getopt : config; getopt(args, - cfg.bundling, - cfg.caseSensitive, + config.bundling, + config.caseSensitive, "a", &aFlag, "C", &CFlag, "F", &FFlag, "f", &fFlag, @@ -104,7 +102,7 @@ main(string[] args) } if (VFlag) { - writeln("esv " ~ VERSION); + writeln("esv " ~ cf.esvVersion); return 0; } @@ -127,7 +125,7 @@ main(string[] args) * Options have first priority, then environment variables, * then the default path */ config: - configPath = environment.get(ENV_CONFIG, DEFAULT_CONFIGPATH).expandTilde(); + configPath = environment.get(cf.configEnv, cf.configPath).expandTilde(); try { if (CFlag != "") { /* if -C was given */ enforceDie(isValidPath(CFlag), CFlag ~ ": invalid path"); @@ -151,7 +149,7 @@ key = %s #headings = false #passage-references = false #verse-numbers = false -"(DEFAULT_APIKEY)); +"(cf.apiKey)); } } readINIFile(ini, configPath); @@ -175,7 +173,7 @@ key = %s tmpf = esv.getAudioPassage(args[1], args[2]); catch (CurlException e) die(e.msg); - mpegPlayer = environment.get(ENV_PLAYER, DEFAULT_MPEGPLAYER); + mpegPlayer = environment.get(cf.playerEnv, cf.mp3Player); /* check for an audio player */ enforceDie( From 6096e01e01cc7e7cab13a060f4a452b17ba72ff0 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Tue, 25 Jun 2024 15:53:39 +1200 Subject: [PATCH 080/133] esv: rename mpegPlayer to player --- esv.d | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/esv.d b/esv.d index e6ed9c4..4395de9 100644 --- a/esv.d +++ b/esv.d @@ -167,30 +167,30 @@ key = %s esv = new ESVApi(apiKey); if (aFlag) { - string tmpf, mpegPlayer; + string tmpf, player; try tmpf = esv.getAudioPassage(args[1], args[2]); catch (CurlException e) die(e.msg); - mpegPlayer = environment.get(cf.playerEnv, cf.mp3Player); + player = environment.get(cf.playerEnv, cf.mp3Player); /* check for an audio player */ enforceDie( executeShell( - format!"command -v %s >/dev/null 2>&1"(mpegPlayer) + format!"command -v %s >/dev/null 2>&1"(player) ).status == 0, - mpegPlayer ~ " is required for audio mode; cannot continue"); + 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 */ - mpegPlayer ~= - mpegPlayer == "mpg123" ? " -q " : - mpegPlayer == "mpv" ? " --msg-level=all=no " : " "; + player ~= + player == "mpg123" ? " -q " : + player == "mpv" ? " --msg-level=all=no " : " "; /* spawn the player */ - executeShell(mpegPlayer ~ tmpf); + executeShell(player ~ tmpf); return 0; } From 36c520e00f9cdaafe0ec2dbdaa909662735d0b02 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Tue, 25 Jun 2024 16:04:27 +1200 Subject: [PATCH 081/133] README.md: revise content --- README.md | 39 +++++++++++++++++---------------------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index a785007..6cc1a3c 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,10 @@ [![builds.sr.ht status](https://builds.sr.ht/~jeremy/esv.svg)](https://builds.sr.ht/~jeremy/esv) -`esv` displays passages of the English Standard Bible on your -terminal. It connects to the ESV web API to retrieve the passages, and -allows configuration through command-line options and the -configuration file. - -Example usage: +`esv` displays passages of the English Standard Version of the Bible on +your terminal. It connects to the ESV web API to retrieve the passages, +and allows configuration with command line options and the configuration +file. ``` $ esv Psalm 23 @@ -21,25 +19,23 @@ A Psalm of David. He makes me lie down in green pastures.... ``` -The names of Bible books are not case sensitive, so John, john, and -JOHN are all accepted. +The names of Bible books are case-insensitive, so John, john, +and JOHN are all accepted. ## Audio -esv supports playing audio passages through the -a option. The -`mpg123` audio/video player is utilised here and so it is required if -you want to play audio passages. If you prefer, you can use a -different player (such as mpv) by editing config.di before compiling. +esv supports playing audio passages through the -a flag. The +mpg123 audio player is used for playing audio by default but +you can set the `ESV_PLAYER` environment variable to something +else such as mpv if you don't have/don't want to use mpg123. -Using the program to play audio is mostly the same as normal text -usage. `esv -a Matthew 5-7` will play an audio passage of Matthew 5-7. +Playing audio is mostly the same as reading a normal text passage. +`esv -a Matthew 5-7` will play an audio passage of Matthew 5-7. ## Installation -To install esv, first make sure you have a D compiler installed on -your system. -ldc, the LLVM D compiler (https://github.com/ldc-developers/ldc#installation) -is recommended but dmd is also supported. +To install esv, first make sure you have a D compiler installed. +LDC is recommended but you can also use DMD. Commands prefixed with a dollar sign ($) are intended to be run as a standard user, and commands prefixed with a hash sign (#) are intended @@ -67,10 +63,9 @@ configuration file respectively. ## Copying -Copying, modifying and redistributing this software is permitted as -long as your modified version conforms to the GNU General Public -License version 2. +Copying, modifying and redistributing this software is permitted +under the terms of the GNU General Public License version 2. -Full licenses are contained in the COPYING file. +Full license texts are contained in the COPYING file. Copyright (c) 2023-2024 Jeremy Baxter From 1ba023122a7c612f692df721086b49ce122b18c1 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Tue, 25 Jun 2024 16:19:57 +1200 Subject: [PATCH 082/133] esv: revise comments --- esv.d | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/esv.d b/esv.d index 4395de9..a3adea0 100644 --- a/esv.d +++ b/esv.d @@ -121,9 +121,8 @@ main(string[] args) enforceDie(verseValid(args[2]), "invalid verse format '%s'", args[2]); - /* determine configuration file - * Options have first priority, then environment variables, - * then the default path */ + /* 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 { @@ -183,9 +182,7 @@ key = %s 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 */ + * Other players will work, just set ESV_PLAYER */ player ~= player == "mpg123" ? " -q " : player == "mpv" ? " --msg-level=all=no " : " "; From 1ed36260f351324bea6cf0a82fe7493a0070e2d9 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Tue, 25 Jun 2024 16:20:40 +1200 Subject: [PATCH 083/133] esv: remove bad use of UFCS --- esv.d | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/esv.d b/esv.d index a3adea0..e3bbb64 100644 --- a/esv.d +++ b/esv.d @@ -93,10 +93,12 @@ main(string[] args) "V", &VFlag, ); } catch (GetOptException e) { + string opt = extractOpt(e); + enforceDie(!e.msg.startsWith("Unrecognized option"), - "unknown option " ~ e.extractOpt()); + "unknown option " ~ opt); enforceDie(!e.msg.startsWith("Missing value for argument"), - "missing argument for option " ~ e.extractOpt()); + "missing argument for option " ~ opt); die(e.msg); /* catch-all */ } @@ -112,11 +114,13 @@ main(string[] args) } if (args.length < 3) { - stderr.writefln("usage: %s [-aFfHhNnRrV] [-C config] [-l length] [-s query] book verses", args[0].baseName()); + stderr.writefln( + "usage: %s [-aFfHhNnRrV] [-C config] [-l length] [-s query] book verses", + baseName(args[0])); return 1; } - enforceDie(bookValid(args[1].parseBook()), + enforceDie(bookValid(parseBook(args[1])), "book '%s' does not exist", args[1]); enforceDie(verseValid(args[2]), "invalid verse format '%s'", args[2]); @@ -124,17 +128,18 @@ main(string[] args) /* determine configuration file: options take first priority, * then environment variables, and then the default path */ config: - configPath = environment.get(cf.configEnv, cf.configPath).expandTilde(); + configPath = environment.get(cf.configEnv, cf.configPath) + .expandTilde(); try { if (CFlag != "") { /* if -C was given */ - enforceDie(isValidPath(CFlag), CFlag ~ ": invalid path"); + enforceDie(CFlag.isValidPath(), CFlag ~ ": invalid path"); configPath = CFlag.expandTilde(); } else { - enforceDie(isValidPath(configPath), + enforceDie(configPath.isValidPath(), configPath ~ ": invalid path"); if (!configPath.exists()) { - mkdirRecurse(configPath.dirName()); + mkdirRecurse(dirName(configPath)); configPath.write(format! "## Configuration file for esv. @@ -234,7 +239,7 @@ key = %s if (RFlag) esv.opts.b["include-passage-references"] = false; try - writeln(esv.getPassage(args[1].parseBook(), args[2])); + writeln(esv.getPassage(parseBook(args[1]), args[2])); catch (CurlException e) die(e.msg); return 0; From d8e267d5ba033844cbde441022a05edb2e19bbe8 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Tue, 25 Jun 2024 16:43:21 +1200 Subject: [PATCH 084/133] esv: split utility functions into separate module --- configure | 4 +-- esv.d | 78 +++--------------------------------------------- util.d | 89 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 95 insertions(+), 76 deletions(-) create mode 100644 util.d diff --git a/configure b/configure index 3f9e07c..38055a6 100755 --- a/configure +++ b/configure @@ -5,8 +5,8 @@ set -e mkf=config.mk -objs='esv.o esvapi.o initial.o' -srcs='esv.d esvapi.d initial.d' +objs='esv.o esvapi.o util.o initial.o' +srcs='esv.d esvapi.d util.d initial.d' # utility functions diff --git a/esv.d b/esv.d index e3bbb64..ee67571 100644 --- a/esv.d +++ b/esv.d @@ -27,11 +27,12 @@ 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.stdio : writeln, writefln; import std.string : splitLines; import esvapi; import initial; +import util; import cf = config; @@ -48,13 +49,6 @@ 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) { @@ -63,18 +57,7 @@ main(string[] args) 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"); + sharedInit(args); /* Parse command-line options */ try { @@ -93,7 +76,7 @@ main(string[] args) "V", &VFlag, ); } catch (GetOptException e) { - string opt = extractOpt(e); + string opt = extractFlag(e); enforceDie(!e.msg.startsWith("Unrecognized option"), "unknown option " ~ opt); @@ -245,51 +228,6 @@ key = %s 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) { @@ -299,11 +237,3 @@ onLineLength(string flag, string value) 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("-_", " "); -} diff --git a/util.d b/util.d new file mode 100644 index 0000000..826fff7 --- /dev/null +++ b/util.d @@ -0,0 +1,89 @@ +module util; + +import std.getopt : GetOptException; +import std.stdio : File; + +public @safe: + +File stderr; + +private { + string[] mainArgs; + + version (OpenBSD) { + immutable(char) *promises; + } +} + +/++ + + Common initialisation function shared between esv and esvsearch + +/ +void +sharedInit(string[] args) +{ + mainArgs = args; + stderr = File("/dev/stderr", "w"); + + 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); + }(); +} + +void +enforceDie(A...)(bool cond, string fmt, A a) +{ + import std.format : format; + + if (!cond) + die(format(fmt, a)); +} + +string +extractFlag(in GetOptException e) +{ + import std.regex : matchFirst; + + return e.msg.matchFirst("-.")[0]; +} + +string +parseBook(in string book) +{ + import std.string : tr; + + return book.tr("-_", " "); +} + +ushort +terminalColumns() @trusted +{ + import core.sys.posix.sys.ioctl; + + winsize w; + + ioctl(1, TIOCGWINSZ, &w); + return w.ws_col > 72 ? 72 : w.ws_col; +} + +void +warn(string mesg) +{ + import std.path : baseName; + + stderr.writeln(baseName(mainArgs[0]) ~ ": " ~ mesg); +} + +void +die(string mesg) @trusted +{ + import core.runtime : Runtime; + import core.stdc.stdlib : exit; + + warn(mesg); + Runtime.terminate(); + exit(1); +} From 7a9866a4e20d235954cb2cf91878b401e279a6eb Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Tue, 25 Jun 2024 17:00:02 +1200 Subject: [PATCH 085/133] util: split getopt error handling into utility function --- esv.d | 10 +--------- util.d | 17 ++++++++++++----- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/esv.d b/esv.d index ee67571..c1b8bc1 100644 --- a/esv.d +++ b/esv.d @@ -20,7 +20,6 @@ module esv; -import std.algorithm : startsWith; import std.conv : to, ConvException; import std.file : exists, mkdirRecurse, write, FileException; import std.format : format; @@ -76,14 +75,7 @@ main(string[] args) "V", &VFlag, ); } catch (GetOptException e) { - string opt = extractFlag(e); - - enforceDie(!e.msg.startsWith("Unrecognized option"), - "unknown option " ~ opt); - enforceDie(!e.msg.startsWith("Missing value for argument"), - "missing argument for option " ~ opt); - - die(e.msg); /* catch-all */ + handleOptError(e.msg); } if (VFlag) { diff --git a/util.d b/util.d index 826fff7..044e2c8 100644 --- a/util.d +++ b/util.d @@ -1,7 +1,6 @@ module util; -import std.getopt : GetOptException; -import std.stdio : File; +import std.stdio : File; public @safe: @@ -42,12 +41,20 @@ enforceDie(A...)(bool cond, string fmt, A a) die(format(fmt, a)); } -string -extractFlag(in GetOptException e) +void +handleOptError(in string msg) { + import std.algorithm : startsWith; import std.regex : matchFirst; - return e.msg.matchFirst("-.")[0]; + string opt = msg.matchFirst("-.")[0]; + + enforceDie(!msg.startsWith("Unrecognized option"), + "unknown option " ~ opt); + enforceDie(!msg.startsWith("Missing value for argument"), + "missing argument for option " ~ opt); + + die(msg); /* catch-all */ } string From bf3eb2953a09ce22a34c9c65de4bbc6537170599 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Tue, 25 Jun 2024 17:28:36 +1200 Subject: [PATCH 086/133] esv: strip trailing whitespace from header --- esv.d | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esv.d b/esv.d index c1b8bc1..1ff6e0b 100644 --- a/esv.d +++ b/esv.d @@ -3,17 +3,17 @@ * * 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 . */ From f540160ff387ccefb2bf06b6faaae8d8873e3ee6 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Wed, 26 Jun 2024 09:25:01 +1200 Subject: [PATCH 087/133] esv: remove invalid path checks They're catering to too much of an edge case. It can just be checked with std.file.exists() later on. --- esv.d | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/esv.d b/esv.d index 1ff6e0b..c322b01 100644 --- a/esv.d +++ b/esv.d @@ -24,7 +24,7 @@ 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.path : baseName, dirName, expandTilde; import std.process : environment, executeShell; import std.stdio : writeln, writefln; import std.string : splitLines; @@ -107,12 +107,8 @@ config: .expandTilde(); try { if (CFlag != "") { /* if -C was given */ - enforceDie(CFlag.isValidPath(), CFlag ~ ": invalid path"); configPath = CFlag.expandTilde(); } else { - enforceDie(configPath.isValidPath(), - configPath ~ ": invalid path"); - if (!configPath.exists()) { mkdirRecurse(dirName(configPath)); configPath.write(format! From 8f699c3212f3762e877b704e4546f32a519c8d22 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Wed, 26 Jun 2024 09:40:51 +1200 Subject: [PATCH 088/133] esv: rename -C flag to -c The capital C flag is often associated with changing directories. The tar and make programs use it this way. --- esv.1 | 6 +++--- esv.d | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/esv.1 b/esv.1 index 226aefd..453b68f 100644 --- a/esv.1 +++ b/esv.1 @@ -1,4 +1,4 @@ -.Dd $Mdocdate: June 25 2024 $ +.Dd $Mdocdate: June 26 2024 $ .Dt ESV 1 .Os .Sh NAME @@ -8,7 +8,7 @@ .Nm esv .Bk -words .Op Fl aFfHhNnRrV -.Op Fl C Ar config +.Op Fl c Ar config .Op Fl l Ar length .Op Fl s Ar query .Ar book verses @@ -51,7 +51,7 @@ The options are as follows: .Bl -tag -width 123456 .It Fl a Play a recorded audio track rather than showing a passage. -.It Fl C Ar configfile +.It Fl c Ar configfile Read the configuration from the path .Ar configfile . This overrides the diff --git a/esv.d b/esv.d index c322b01..25683e2 100644 --- a/esv.d +++ b/esv.d @@ -38,7 +38,7 @@ import cf = config; @safe: bool aFlag; /* audio */ -string CFlag; /* config */ +string cFlag; /* config path */ bool fFlag, FFlag; /* footnotes */ bool hFlag, HFlag; /* headings */ int lFlag; /* line length */ @@ -65,7 +65,7 @@ main(string[] args) config.bundling, config.caseSensitive, "a", &aFlag, - "C", &CFlag, + "c", &cFlag, "F", &FFlag, "f", &fFlag, "H", &HFlag, "h", &hFlag, "l", &onLineLength, @@ -90,7 +90,7 @@ main(string[] args) if (args.length < 3) { stderr.writefln( - "usage: %s [-aFfHhNnRrV] [-C config] [-l length] [-s query] book verses", + "usage: %s [-aFfHhNnRrV] [-c config] [-l length] [-s query] book verses", baseName(args[0])); return 1; } @@ -106,8 +106,8 @@ config: configPath = environment.get(cf.configEnv, cf.configPath) .expandTilde(); try { - if (CFlag != "") { /* if -C was given */ - configPath = CFlag.expandTilde(); + if (cFlag != "") { /* if -c was given */ + configPath = cFlag.expandTilde(); } else { if (!configPath.exists()) { mkdirRecurse(dirName(configPath)); From 3507d9ca242741487f65dbef2d78fc38a0dee641 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Wed, 26 Jun 2024 09:45:52 +1200 Subject: [PATCH 089/133] esv: initialise cFlag to null Before this change a user could pass an empty string as the argument to -C and it would go undetected. --- esv.d | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/esv.d b/esv.d index 25683e2..186902f 100644 --- a/esv.d +++ b/esv.d @@ -58,6 +58,8 @@ main(string[] args) sharedInit(args); + cFlag = null; + /* Parse command-line options */ try { import std.getopt : config; @@ -106,7 +108,7 @@ config: configPath = environment.get(cf.configEnv, cf.configPath) .expandTilde(); try { - if (cFlag != "") { /* if -c was given */ + if (cFlag) { configPath = cFlag.expandTilde(); } else { if (!configPath.exists()) { From 692d413bfa04884c4e2bb736347e170a35dcc257 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Wed, 26 Jun 2024 09:57:10 +1200 Subject: [PATCH 090/133] esv: improve configuration path code --- esv.d | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/esv.d b/esv.d index 186902f..dc2e7f6 100644 --- a/esv.d +++ b/esv.d @@ -108,12 +108,12 @@ config: configPath = environment.get(cf.configEnv, cf.configPath) .expandTilde(); try { - if (cFlag) { + if (cFlag) configPath = cFlag.expandTilde(); - } else { - if (!configPath.exists()) { - mkdirRecurse(dirName(configPath)); - configPath.write(format! + + if (!configPath.exists()) { + mkdirRecurse(dirName(configPath)); + configPath.write(format! "## Configuration file for esv. # An API key is required to access the ESV Bible API. @@ -127,7 +127,6 @@ key = %s #passage-references = false #verse-numbers = false "(cf.apiKey)); - } } readINIFile(ini, configPath); } catch (FileException e) { From de71043ef1faf991ee24b4d1c79471daa1c38f69 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Wed, 26 Jun 2024 12:41:41 +1200 Subject: [PATCH 091/133] esv.1: rename argument for -c In the synopsis it is named config. --- esv.1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esv.1 b/esv.1 index 453b68f..84b5b02 100644 --- a/esv.1 +++ b/esv.1 @@ -51,9 +51,9 @@ The options are as follows: .Bl -tag -width 123456 .It Fl a Play a recorded audio track rather than showing a passage. -.It Fl c Ar configfile +.It Fl c Ar config Read the configuration from the path -.Ar configfile . +.Ar config . This overrides the .Ev ESV_CONFIG environment variable (see section From ec8be68b49f0676f9e19e981e99c6807cd8451af Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Wed, 26 Jun 2024 12:55:41 +1200 Subject: [PATCH 092/133] esvsearch: split esv search code into separate program Requires rewiring the build system to accommodate for two executables. Fixes: https://todo.sr.ht/~jeremy/esv/4 --- .gitignore | 1 + Makefile | 13 ++++--- configure | 4 +- esv.1 | 5 --- esv.d | 21 +--------- esvsearch.d | 110 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 122 insertions(+), 32 deletions(-) create mode 100644 esvsearch.d diff --git a/.gitignore b/.gitignore index 2564793..a2666f7 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ *.so *.a esv +esvsearch result config.mk \ No newline at end of file diff --git a/Makefile b/Makefile index 25e82a6..009925f 100644 --- a/Makefile +++ b/Makefile @@ -6,12 +6,14 @@ DC = ${_DC} CFLAGS = ${_CFLAGS} OBJS = ${_OBJS} -all: esv +all: esv esvsearch include config.mk -esv: ${OBJS} - ${DC} ${_LDFLAGS} -of=$@ ${OBJS} +esv: esv.o ${OBJS} + ${DC} ${_LDFLAGS} -of=$@ esv.o ${OBJS} +esvsearch: esvsearch.o ${OBJS} + ${DC} ${_LDFLAGS} -of=$@ esvsearch.o ${OBJS} .SUFFIXES: .d .o @@ -19,10 +21,11 @@ esv: ${OBJS} ${DC} ${CFLAGS} -c $< clean: - rm -f esv ${OBJS} ${INIOBJS} + rm -f esv esvsearch esv.o esvsearch.o ${OBJS} -install: esv +install: esv esvsearch install -Dm755 esv ${DESTDIR}${PREFIX}/bin/esv + install -Dm755 esvsearch ${DESTDIR}${PREFIX}/bin/esvsearch install -Dm644 esv.1 ${DESTDIR}${MANPREFIX}/man1/esv.1 install -Dm644 esv.conf.5 ${DESTDIR}${MANPREFIX}/man5/esv.conf.5 diff --git a/configure b/configure index 38055a6..9eb816c 100755 --- a/configure +++ b/configure @@ -5,8 +5,8 @@ set -e mkf=config.mk -objs='esv.o esvapi.o util.o initial.o' -srcs='esv.d esvapi.d util.d initial.d' +objs='esvapi.o util.o initial.o' +srcs='esvapi.d util.d initial.d' # utility functions diff --git a/esv.1 b/esv.1 index 84b5b02..35257e8 100644 --- a/esv.1 +++ b/esv.1 @@ -10,7 +10,6 @@ .Op Fl aFfHhNnRrV .Op Fl c Ar config .Op Fl l Ar length -.Op Fl s Ar query .Ar book verses .Ek .Sh DESCRIPTION @@ -96,10 +95,6 @@ Include verse numbers (the default). Exclude passage references. .It Fl r Include passage references (the default). -.It Fl s Ar query -Rather than displaying a passage or playing an audio track, -search the Bible for instances of -.Ar query . .It Fl V Print the version number and exit. .El diff --git a/esv.d b/esv.d index dc2e7f6..c173feb 100644 --- a/esv.d +++ b/esv.d @@ -45,7 +45,6 @@ int lFlag; /* line length */ bool lFlagSpecified; bool nFlag, NFlag; /* verse numbers */ bool rFlag, RFlag; /* passage references */ -string sFlag; /* search passages */ bool VFlag; /* show version */ int @@ -73,7 +72,6 @@ main(string[] args) "l", &onLineLength, "N", &NFlag, "n", &nFlag, "R", &RFlag, "r", &rFlag, - "s", &sFlag, "V", &VFlag, ); } catch (GetOptException e) { @@ -85,14 +83,9 @@ main(string[] args) 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", + "usage: %s [-aFfHhNnRrV] [-c config] [-l length] book verses", baseName(args[0])); return 1; } @@ -134,8 +127,6 @@ key = %s 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"); @@ -168,16 +159,6 @@ key = %s 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 */ diff --git a/esvsearch.d b/esvsearch.d new file mode 100644 index 0000000..72ffba0 --- /dev/null +++ b/esvsearch.d @@ -0,0 +1,110 @@ +/* + * esvsearch: search 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 . + */ + +module esvsearch; + +import std.file : FileException; +import std.getopt : getopt, GetOptException; +import std.path : baseName, expandTilde; +import std.process : environment; +import std.stdio : writeln, writefln; + +import esvapi; +import initial; +import util; + +import cf = config; + +@safe: + +string cFlag; /* config path */ +bool VFlag; /* show version */ + +int +main(string[] args) +{ + string apiKey; + string configPath; + INIUnit ini; + ESVApi esv; + + sharedInit(args); + + cFlag = null; + + /* Parse command-line options */ + try { + import std.getopt : config; + getopt(args, + config.bundling, + config.caseSensitive, + "c", &cFlag, + "V", &VFlag, + ); + } catch (GetOptException e) { + handleOptError(e.msg); + } + + if (VFlag) { + writeln("esvsearch " ~ cf.esvVersion); + return 0; + } + + if (args.length < 2) { + stderr.writefln("usage: %s [-l length] query", + baseName(args[0])); + return 1; + } + + /* determine configuration file: options take first priority, + * then environment variables, and then the default path */ + configPath = environment.get(cf.configEnv, cf.configPath) + .expandTilde(); + try { + if (cFlag) + configPath = cFlag.expandTilde(); + + readINIFile(ini, configPath); + } catch (FileException e) { + /* filesystem syscall errors */ + import core.stdc.errno : ENOENT; + + if (e.errno != ENOENT) + die(e.msg); + + warn(configPath ~ ": no such file or directory"); + warn("Invoke esv to create an initial configuration file."); + } + + apiKey = ini["api"].key("key"); + enforceDie(apiKey != null, + "API key not present in configuration file; cannot proceed"); + + esv = new ESVApi(apiKey); + + try + writeln(esv.searchFormat(args[1])); + catch (ESVException) + die("no results"); + catch (CurlException e) + die(e.msg); + + return 0; +} From df6abbf50c37e0751c3f39aeaa69c62be5325c07 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Wed, 26 Jun 2024 13:03:35 +1200 Subject: [PATCH 093/133] esvsearch.1: init man page --- Makefile | 1 + esvsearch.1 | 72 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 esvsearch.1 diff --git a/Makefile b/Makefile index 009925f..766e837 100644 --- a/Makefile +++ b/Makefile @@ -27,6 +27,7 @@ install: esv esvsearch install -Dm755 esv ${DESTDIR}${PREFIX}/bin/esv install -Dm755 esvsearch ${DESTDIR}${PREFIX}/bin/esvsearch install -Dm644 esv.1 ${DESTDIR}${MANPREFIX}/man1/esv.1 + install -Dm644 esvsearch.1 ${DESTDIR}${MANPREFIX}/man1/esvsearch.1 install -Dm644 esv.conf.5 ${DESTDIR}${MANPREFIX}/man5/esv.conf.5 .PHONY: all clean install diff --git a/esvsearch.1 b/esvsearch.1 new file mode 100644 index 0000000..c8a1046 --- /dev/null +++ b/esvsearch.1 @@ -0,0 +1,72 @@ +.Dd $Mdocdate: June 26 2024 $ +.Dt ESVSEARCH 1 +.Os +.Sh NAME +.Nm esvsearch +.Nd search the Bible +.Sh SYNOPSIS +.Nm esvsearch +.Bk -words +.Op Fl V +.Op Fl c Ar config +.Ar query +.Ek +.Sh DESCRIPTION +.Nm +searches the Bible for the given +.Ar query . +.Pp +Like +.Xr esv 1 , +.Nm +requires a configuration file to operate. +This is required to know the API key to use +when accessing the ESV Bible online. +.Nm +uses the same configuration file as esv, +and it uses the same heuristics in determining the configuration file as well, +except that it does not +create a configuration file automatically +if one does not already exist; +you will need to invoke +.Xr esv 1 +for the first time before running +.Nm . +.Bl -tag -width 123456 +.It Fl c Ar config +Read the configuration from the path +.Ar config . +See the documentation on +.Fl c +in +.Xr esv 1 +for more information. +.Sx ENVIRONMENT ) . +.It Fl V +Print the version number and exit. +.El +.Sh EXAMPLES +Search the Bible for verses containing +.Dq rabble : +.Pp +.Dl esvsearch rabble +.Pp +.Sh SEE ALSO +.Xr esv 1 , +.Xr esv.conf 5 +.Sh AUTHORS +.An Jeremy Baxter Aq Mt jeremy@baxters.nz +.Sh BUGS +.Nm +outputs search results in a human-readable manner, +which makes it nice for humans to read but difficult +for machines to parse and store. +.Nm +should support outputting results in a shell-readable format +and the JSON format. +.Pp +If you think you've found a potential bug, +please report it to me using my email address above. +.Pp +Existing bugs and planned features can be found on the bug tracker: +.Lk https://todo.sr.ht/~jeremy/esv From 91dc7cd07eeb0faa560fff664561b379291741bc Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Wed, 26 Jun 2024 13:10:57 +1200 Subject: [PATCH 094/133] esvapi: style fixes --- esvapi.d | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/esvapi.d b/esvapi.d index 0356120..bceb9e1 100644 --- a/esvapi.d +++ b/esvapi.d @@ -41,11 +41,8 @@ enum ESVIndent TAB } -/++ Default URL to use when sending API requests. +/ -enum ESVAPI_URL = "https://api.esv.org/v3/passage"; - /++ Constant array of all books in the Bible. +/ -immutable string[] BIBLE_BOOKS = [ +immutable string[] bibleBooks = [ /* Old Testament */ "Genesis", "Exodus", @@ -66,7 +63,6 @@ immutable string[] BIBLE_BOOKS = [ "Esther", "Job", "Psalm", - "Psalms", /* both are valid */ "Proverbs", "Ecclesiastes", "Song of Solomon", @@ -147,7 +143,7 @@ immutable string[] ESVAPI_PARAMETERS = [ bool bookValid(in char[] book) nothrow { - foreach (string b; BIBLE_BOOKS) { + foreach (string b; bibleBooks) { if (book.capitalize() == b.capitalize()) return true; } @@ -202,7 +198,7 @@ class ESVApi { _key = key; _tmp = tempDir() ~ tmpName; - _url = ESVAPI_URL; + _url = "https://api.esv.org/v3/passage"; opts = ESVApiOptions(true); extraParameters = ""; onProgress = delegate int (size_t dlTotal, size_t dlNow, From 7eb179255fd48dd9beecc29f6595a6e01cc70564 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Wed, 26 Jun 2024 13:13:50 +1200 Subject: [PATCH 095/133] builds/openbsd.yml: move to 7.4 OpenBSD 7.5 broke the dmd and ldc packages. They're back working in -current as of now. --- .builds/openbsd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.builds/openbsd.yml b/.builds/openbsd.yml index caeffc6..c06e7f2 100644 --- a/.builds/openbsd.yml +++ b/.builds/openbsd.yml @@ -1,5 +1,5 @@ --- -image: openbsd/latest +image: openbsd/7.4 packages: - curl - ldc From a2be024c453a79f982c9de15ebf9516c576bef30 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Wed, 26 Jun 2024 13:21:25 +1200 Subject: [PATCH 096/133] esvapi: fix compile error on LDC 1.33.0 --- esvapi.d | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/esvapi.d b/esvapi.d index bceb9e1..b4764fc 100644 --- a/esvapi.d +++ b/esvapi.d @@ -330,9 +330,11 @@ class ESVApi JSONValue makeQuery(long page) - => parseJSON(makeRequest("search/?page-size=100" + { + return parseJSON(makeRequest("search/?page-size=100" ~ "&page=" ~ page.to!string() ~ "&q=" ~ query.tr(" ", "+"))); + } pages ~= makeQuery(1); if (pages[0]["total_pages"].integer == 1) { From f353279458200d820f4c0e11d0fb825b7520a7ab Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Sat, 29 Jun 2024 20:41:14 +1200 Subject: [PATCH 097/133] esvapi: refactor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 18 insertions, 68 deletions! (🚀) Fixes: https://todo.sr.ht/~jeremy/esv/6 --- esv.d | 2 +- esvapi.d | 82 +++++++++++------------------------------------------ esvsearch.d | 2 +- 3 files changed, 18 insertions(+), 68 deletions(-) diff --git a/esv.d b/esv.d index c173feb..30642fa 100644 --- a/esv.d +++ b/esv.d @@ -131,7 +131,7 @@ key = %s enforceDie(apiKey != null, "API key not present in configuration file; cannot proceed"); - esv = new ESVApi(apiKey); + esv = ESVApi(apiKey); if (aFlag) { string tmpf, player; diff --git a/esvapi.d b/esvapi.d index b4764fc..3809273 100644 --- a/esvapi.d +++ b/esvapi.d @@ -171,73 +171,24 @@ verseValid(in char[] verse) } /++ - + ESV API object containing the authentication key, - + the API URL, any parameters to use when contacting the - + API as well as the temporary directory to use when - + fetching audio passages. + + Structure containing the authentication key, API URL, + + any parameters to use when making a request as well as the + + temporary directory to use when fetching audio passages. +/ -class ESVApi +struct ESVApi { - protected { - string _key; - string _tmp; - string _url; - } - ESVApiOptions opts; + string key; /++ API key +/ + string tmp; /++ Tempfile directory +/ + string url; /++ API URL +/ + string extraParameters; /++ Additional request parameters +/ - /++ Additional request parameters +/ - string extraParameters; - /++ Called whenever progress is made on a request. +/ - int delegate(size_t, size_t, size_t, size_t) onProgress; - - /++ - + Constructs an ESVApi object using the given authentication key. - +/ - this(string key, string tmpName = "esv") + this(string apiKey) { - _key = key; - _tmp = tempDir() ~ tmpName; - _url = "https://api.esv.org/v3/passage"; + key = apiKey; + tmp = tempDir() ~ "esv"; + url = "https://api.esv.org/v3/passage"; opts = ESVApiOptions(true); - extraParameters = ""; - onProgress = delegate int (size_t dlTotal, size_t dlNow, - size_t ulTotal, size_t ulNow) - { - return 0; - }; - } - - /++ - + Returns the API authentication key that was given when the object - + was constructed. This authentication key cannot be changed. - +/ - final @property string - key() const nothrow pure @nogc - => _key; - - /++ - + Returns the subdirectory used to store temporary audio passages. - +/ - final @property string - tmpDir() const nothrow pure @nogc - => _tmp; - - /++ - + Returns the API URL currently in use. - +/ - final @property string - url() const nothrow pure @nogc - => _url; - - /++ - + Sets the API URL currently in use to the given url argument. - +/ - final @property void - url(immutable(string) url) - in (!url.matchAll(`^https?://.+\\..+(/.+)?`).empty, "Invalid URL format") - { - _url = url; } /++ @@ -308,11 +259,11 @@ class ESVApi { File tmpFile; - tmpFile = File(_tmp, "w"); + tmpFile = File(tmp, "w"); tmpFile.write(makeRequest(format!"audio/?q=%s+%s"( book.capitalize().tr(" ", "+"), verse))); - return _tmp; + return tmp; } /++ @@ -397,15 +348,14 @@ class ESVApi HTTP request; response = []; - request = HTTP(_url ~ "/" ~ query); - request.onProgress = onProgress; + request = HTTP(url ~ "/" ~ query); request.onReceive = (ubyte[] data) { response ~= data; return data.length; }; - request.addRequestHeader("Authorization", "Token " ~ _key); + request.addRequestHeader("Authorization", "Token " ~ key); request.perform(); return response; diff --git a/esvsearch.d b/esvsearch.d index 72ffba0..056a346 100644 --- a/esvsearch.d +++ b/esvsearch.d @@ -97,7 +97,7 @@ main(string[] args) enforceDie(apiKey != null, "API key not present in configuration file; cannot proceed"); - esv = new ESVApi(apiKey); + esv = ESVApi(apiKey); try writeln(esv.searchFormat(args[1])); From aba8532be491249bf4257a99438b53cd98d84a8d Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Wed, 3 Jul 2024 11:15:37 +1200 Subject: [PATCH 098/133] esv.1: document hyphens in book names --- esv.1 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/esv.1 b/esv.1 index 35257e8..d8b58c1 100644 --- a/esv.1 +++ b/esv.1 @@ -29,11 +29,11 @@ and .Em chapter:verse-verse . If the name of your desired book has a space in it, e.g. .Dq "1 Corinthians" , -you can put an underscore in the place of the space, -or you can just pass the full book name with the space +you can put a hyphen or underscore in the place of the space, +or you can just pass the full book name with the space in it by surrounding the argument with quotes in your shell. -Both -.Dq 1_Corinthians +Thus, both +.Dq 1-Corinthians and .Dq "1 Corinthians" are valid book names. From e1cf13c8d76e40fe5ac240847d5a06bd1f918a21 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Wed, 3 Jul 2024 11:18:00 +1200 Subject: [PATCH 099/133] esv: substitute hyphens in books with -a Fixes: https://todo.sr.ht/~jeremy/esv/7 --- esv.d | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esv.d b/esv.d index 30642fa..ea74e88 100644 --- a/esv.d +++ b/esv.d @@ -137,7 +137,7 @@ key = %s string tmpf, player; try - tmpf = esv.getAudioPassage(args[1], args[2]); + tmpf = esv.getAudioPassage(parseBook(args[1]), args[2]); catch (CurlException e) die(e.msg); player = environment.get(cf.playerEnv, cf.mp3Player); From 9bd36f056f2a23d578aef958ad23a4e5614f9265 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Thu, 18 Jul 2024 19:43:10 +1200 Subject: [PATCH 100/133] esv.el: remove Moved to my elisp repository: https://git.sr.ht/~jeremy/elisp/commit/d92c10581a88bedbdcb78735cd7d1bcbbec3e008 --- .editorconfig | 2 +- esv.el | 53 --------------------------------------------------- 2 files changed, 1 insertion(+), 54 deletions(-) delete mode 100644 esv.el diff --git a/.editorconfig b/.editorconfig index 90cbacb..7615472 100644 --- a/.editorconfig +++ b/.editorconfig @@ -5,6 +5,6 @@ end_of_line = lf indent_style = tab indent_size = 4 -[*.{el,nix,yml}] +[*.{nix,yml}] indent_style = space indent_size = 2 \ No newline at end of file diff --git a/esv.el b/esv.el deleted file mode 100644 index ff3182a..0000000 --- a/esv.el +++ /dev/null @@ -1,53 +0,0 @@ -;;; esv.el --- read the Bible from Emacs -*- lexical-binding:t -*- - -(defgroup esv nil - "Read the Bible." - :prefix "esv-" - :group 'applications) - -(defcustom esv-columns 72 - "Length of each line output by `esv'." - :type 'natnum - :group 'esv) -(defcustom esv-mode-hook nil - "Hook run after entering `esv-mode'." - :type 'hook - :group 'esv) -(defcustom esv-process "esv" - "Name of the process created by `esv'." - :type 'string - :group 'esv) -(defcustom esv-program "esv" - "Path to or name of the program started by `esv'." - :type 'string - :group 'esv) - -(define-derived-mode esv-mode text-mode "ESV-Bible" - "Major mode used for reading the Bible with `esv'." - :group 'esv - - (read-only-mode)) - -(defun esv (book verses) - "Fetch the Bible passage identified by BOOK and VERSES. -The result will be redirected to a buffer specified by `esv-buffer'." - (interactive "MBook: \nMVerses: ") - (let ((buffer (concat book " " verses))) - (catch 'buffer-exists - (when (get-buffer buffer) - (message "Buffer `%s' already exists" buffer) - (throw 'buffer-exists nil)) - ;; execute esv - (call-process esv-program nil buffer t - ;; arguments - (format "-l%d" esv-columns) book verses) - ;; display buffer in another window - (display-buffer buffer) - ;; move point to the beginning of the buffer - (with-current-buffer buffer - (esv-mode) - (goto-char (point-min))) - (set-window-start (get-buffer-window buffer) (point-min)) - t))) - -(provide 'esv) From d020aabbb4114e498a11cd0ed3caea7a922018f5 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Fri, 2 Aug 2024 18:17:30 +1200 Subject: [PATCH 101/133] configure: correct D compiler name capitalisation --- configure | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configure b/configure index 9eb816c..5b08c9a 100755 --- a/configure +++ b/configure @@ -34,7 +34,7 @@ gen_DC () { elif present dmd; then dc=dmd else - throw "D compiler not found; install ldc or dmd" + throw "D compiler not found; install LDC or DMD" fi using "$dc" From 8fc5c8f8d37ebd200c5cb6737d388ddc9b8c24d5 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Fri, 2 Aug 2024 18:32:26 +1200 Subject: [PATCH 102/133] configure: show dependency indicators --- configure | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/configure b/configure index 5b08c9a..6792cd6 100755 --- a/configure +++ b/configure @@ -135,8 +135,9 @@ _LDFLAGS = %s printf '\n# begin generated dependencies\n' i=1 for obj in $objs; do - "$dc" -o- -makedeps \ - "$(printf '%s' "$srcs" | awk '{print $'"$i"'}')" + src="$(printf '%s' "$srcs" | awk '{print $'"$i"'}')" + [ -t 2 ] && printf ' (MK) %s \r' "$src" 1>&2 + "$dc" -o- -makedeps "$src" i="$((i + 1))" done unset i From fc529a3e2390f31535bf85918087c67029a9b11b Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Fri, 2 Aug 2024 18:51:13 +1200 Subject: [PATCH 103/133] configure: sort utility functions alphabetically --- configure | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/configure b/configure index 6792cd6..4e10be0 100755 --- a/configure +++ b/configure @@ -13,13 +13,13 @@ srcs='esvapi.d util.d initial.d' present () { command -v "$1" 1>/dev/null 2>/dev/null } -using () { - >&2 printf "using %s\n" "$1" -} throw () { >&2 printf "%s: %s\n" "$(basename "$0")" "$1" exit 1 } +using () { + >&2 printf "using %s\n" "$1" +} # generators From ddab4da4fb388153016d231ec5117b48d8312266 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Fri, 2 Aug 2024 19:05:12 +1200 Subject: [PATCH 104/133] configure: pregenerate interface files Implements: https://todo.sr.ht/~jeremy/esv/8 --- .gitignore | 2 ++ configure | 16 +++++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index a2666f7..608155e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ *.o *.so *.a + +di/ esv esvsearch result diff --git a/configure b/configure index 4e10be0..4dd8f15 100755 --- a/configure +++ b/configure @@ -5,6 +5,8 @@ set -e mkf=config.mk +di='di' +imports="$di" objs='esvapi.o util.o initial.o' srcs='esvapi.d util.d initial.d' @@ -115,6 +117,18 @@ gen_DC gen_CFLAGS gen_LDFLAGS +for directory in $imports; do + mkdir -p directory + Iflags="$(printf '%s -I%s' "$Iflags" "$directory" | xargs)" +done + +for src in $srcs; do + ! (echo "$src" | grep -Eq '\.d$') \ + && throw "$src: invalid source file extension" + [ -t 2 ] && printf ' (DI) %s \r' "$src" 1>&2 + "$dc" -o- -op -H -Hd="$di" "$src" +done + rm -f "$mkf" { @@ -122,7 +136,7 @@ rm -f "$mkf" _DC = %s _CFLAGS = %s _LDFLAGS = %s -' "$dc" "$cflags" "$ldflags" +' "$dc" "$(echo "$cflags $Iflags" | xargs)" "$ldflags" ## generate obj list printf '_OBJS =' From cb29cc8e8ad52ca23fe3fb11c08834553f7b1e4f Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Fri, 2 Aug 2024 19:49:19 +1200 Subject: [PATCH 105/133] util: add unittest for parseBook() --- util.d | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/util.d b/util.d index 044e2c8..f829f1f 100644 --- a/util.d +++ b/util.d @@ -65,6 +65,12 @@ parseBook(in string book) return book.tr("-_", " "); } +@safe unittest +{ + assert(parseBook("1-Corinthians") == "1 Corinthians"); + assert(parseBook("1_Corinthians") == "1 Corinthians"); +} + ushort terminalColumns() @trusted { From 6436166441e7d2c4505987e7fde6532069e235b3 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Fri, 2 Aug 2024 19:50:22 +1200 Subject: [PATCH 106/133] esvapi: add unittest for verseValid() --- esvapi.d | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/esvapi.d b/esvapi.d index 3809273..294e1a6 100644 --- a/esvapi.d +++ b/esvapi.d @@ -170,6 +170,14 @@ verseValid(in char[] verse) return false; } +@safe unittest +{ + assert(verseValid("1")); + assert(verseValid("5-7")); + assert(verseValid("15:13")); + assert(verseValid("15:12-17")); +} + /++ + Structure containing the authentication key, API URL, + any parameters to use when making a request as well as the From 76e39e9dc8152802b3e2fc7cfbf5deb840fb9b19 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Fri, 2 Aug 2024 19:59:58 +1200 Subject: [PATCH 107/133] configure: mkdir $directory --- configure | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configure b/configure index 4dd8f15..cd4de31 100755 --- a/configure +++ b/configure @@ -118,7 +118,7 @@ gen_CFLAGS gen_LDFLAGS for directory in $imports; do - mkdir -p directory + mkdir -p "$directory" Iflags="$(printf '%s -I%s' "$Iflags" "$directory" | xargs)" done From 76eb783a048d9e685a512a8cf68a7aefcb065fee Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Fri, 2 Aug 2024 20:02:21 +1200 Subject: [PATCH 108/133] configure: accept a full path to the D compiler --- configure | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/configure b/configure index cd4de31..8e2f82a 100755 --- a/configure +++ b/configure @@ -33,8 +33,10 @@ gen_DC () { fi if present ldc2; then dc=ldc2 + dcname="$dc" elif present dmd; then dc=dmd + dcname="$dc" else throw "D compiler not found; install LDC or DMD" fi @@ -45,13 +47,13 @@ gen_DC () { ## flags used in the compilation step gen_CFLAGS () { if [ -z "$debug" ]; then - case "$dc" in + case "$dcname" in ldc2) cflags="-Oz";; dmd) cflags="-O";; esac else cflags="-g" - case "$dc" in + case "$dcname" in ldc2) cflags="$cflags -O0 -d-debug";; dmd) cflags="$cflags -debug";; esac @@ -64,7 +66,7 @@ gen_CFLAGS () { ## flags used in the linking step gen_LDFLAGS () { - if [ "$dc" = ldc2 ]; then + if [ "$dcname" = ldc2 ]; then if present ld.lld; then ldflags="-linker=lld" elif present ld.gold; then @@ -86,10 +88,11 @@ gen_LDFLAGS () { while getopts c:dhr ch; do case "$ch" in c) - case "$OPTARG" in - ldc2) dc="ldc2" ;; - dmd) dc="dmd" ;; - *) throw "unknown D compiler '$OPTARG' specified (valid options: ldc2, dmd)" ;; + dcname="$(basename "$OPTARG")" + case "$dcname" in + ldc2) dc="$OPTARG" ;; + dmd) dc="$OPTARG" ;; + *) throw "unknown D compiler '$dcname' specified (valid options: ldc2, dmd)" ;; esac ;; d) debug=1 ;; From 85b71a6092e6ed459ad826dde71832d6b6adad6c Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Thu, 8 Aug 2024 15:56:54 +1200 Subject: [PATCH 109/133] configure: allow enabling unit tests --- configure | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/configure b/configure index 8e2f82a..6d3b557 100755 --- a/configure +++ b/configure @@ -58,6 +58,7 @@ gen_CFLAGS () { dmd) cflags="$cflags -debug";; esac fi + [ -n "$unittest" ] && cflags="$cflags -unittest -main" for flag in $cflags; do using "$flag" @@ -85,7 +86,7 @@ gen_LDFLAGS () { # command line interface -while getopts c:dhr ch; do +while getopts c:dhrt ch; do case "$ch" in c) dcname="$(basename "$OPTARG")" @@ -97,6 +98,7 @@ while getopts c:dhr ch; do ;; d) debug=1 ;; r) unset debug ;; + t) unittest=1; debug=1 ;; h) cat < Date: Thu, 8 Aug 2024 15:59:27 +1200 Subject: [PATCH 110/133] builds: make -Cesv -> make -C esv --- .builds/archlinux.yml | 2 +- .builds/openbsd.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.builds/archlinux.yml b/.builds/archlinux.yml index bcc71fb..7f20084 100644 --- a/.builds/archlinux.yml +++ b/.builds/archlinux.yml @@ -23,7 +23,7 @@ tasks: ./configure -c ldc2 bmake - install: | - sudo bmake -Cesv install + sudo bmake -C esv install - test: | # very basic test :) esv Matthew 5-7 >/dev/null diff --git a/.builds/openbsd.yml b/.builds/openbsd.yml index c06e7f2..8cbc719 100644 --- a/.builds/openbsd.yml +++ b/.builds/openbsd.yml @@ -11,7 +11,7 @@ tasks: ./configure make - install: | - doas make -Cesv install + doas make -C esv install - test: | # very basic test :) esv Matthew 5-7 >/dev/null From 9e350aea933eff90b62ff9cbed6c86816eaebd1a Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Thu, 8 Aug 2024 16:13:11 +1200 Subject: [PATCH 111/133] builds/nixos.yml: replace archlinux build --- .builds/archlinux.yml | 31 ------------------------------- .builds/nixos.yml | 25 +++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 31 deletions(-) delete mode 100644 .builds/archlinux.yml create mode 100644 .builds/nixos.yml diff --git a/.builds/archlinux.yml b/.builds/archlinux.yml deleted file mode 100644 index 7f20084..0000000 --- a/.builds/archlinux.yml +++ /dev/null @@ -1,31 +0,0 @@ ---- -image: archlinux -packages: - - bmake - - dmd - - curl - - ldc - - nix -sources: - - "https://git.sr.ht/~jeremy/esv" -tasks: - - prepare: | - printf 'experimental-features = nix-command flakes\n' \ - | sudo tee -a /etc/nix/nix.conf - sudo systemctl start nix-daemon - sudo usermod -aG nix-users build - - build-dmd: | - cd esv - ./configure -c dmd - bmake all clean - - build-ldc: | - cd esv - ./configure -c ldc2 - bmake - - install: | - sudo bmake -C esv install - - test: | - # very basic test :) - esv Matthew 5-7 >/dev/null - - flake: | - nix build ./esv diff --git a/.builds/nixos.yml b/.builds/nixos.yml new file mode 100644 index 0000000..0c89a89 --- /dev/null +++ b/.builds/nixos.yml @@ -0,0 +1,25 @@ +--- +image: nixos/unstable +packages: + - nixos.dmd + - nixos.curl + - nixos.gnumake + - nixos.ldc +environment: + NIX_CONFIG: "experimental-features = nix-command flakes" +sources: + - "https://git.sr.ht/~jeremy/esv" +tasks: + - build-dmd: | + cd esv + ./configure -c dmd + make all clean + - build-ldc: | + cd esv + ./configure -c ldc2 + make + - install: | + nix profile install ./esv + - test: | + # very basic test :) + esv Matthew 5-7 >/dev/null From 00b9c836b38450e694b9af23f148dbc5009b163b Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Thu, 8 Aug 2024 16:27:38 +1200 Subject: [PATCH 112/133] builds: add unit testing --- .builds/nixos.yml | 4 ++++ .builds/openbsd.yml | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/.builds/nixos.yml b/.builds/nixos.yml index 0c89a89..98db6a0 100644 --- a/.builds/nixos.yml +++ b/.builds/nixos.yml @@ -21,5 +21,9 @@ tasks: - install: | nix profile install ./esv - test: | + cd esv + ./configure -t + make clean all + ./esv # very basic test :) esv Matthew 5-7 >/dev/null diff --git a/.builds/openbsd.yml b/.builds/openbsd.yml index 8cbc719..59c1043 100644 --- a/.builds/openbsd.yml +++ b/.builds/openbsd.yml @@ -13,5 +13,9 @@ tasks: - install: | doas make -C esv install - test: | + cd esv + ./configure -t + make clean all + ./esv # very basic test :) esv Matthew 5-7 >/dev/null From 663cb91cd3ea30fc2641b5e7096b5383cffb6260 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Thu, 8 Aug 2024 16:55:07 +1200 Subject: [PATCH 113/133] esv, esvsearch: reorder variables --- esv.d | 4 ++-- esvsearch.d | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/esv.d b/esv.d index ea74e88..3db4888 100644 --- a/esv.d +++ b/esv.d @@ -50,10 +50,10 @@ bool VFlag; /* show version */ int main(string[] args) { - string apiKey; - string configPath; INIUnit ini; ESVApi esv; + string apiKey; + string configPath; sharedInit(args); diff --git a/esvsearch.d b/esvsearch.d index 056a346..d0dbcbc 100644 --- a/esvsearch.d +++ b/esvsearch.d @@ -40,10 +40,10 @@ bool VFlag; /* show version */ int main(string[] args) { - string apiKey; - string configPath; INIUnit ini; ESVApi esv; + string apiKey; + string configPath; sharedInit(args); From 140376887a7c6dfcbd3b2db16b82802e361758fb Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Thu, 8 Aug 2024 16:57:47 +1200 Subject: [PATCH 114/133] esvsearch: extract query into variable --- esvsearch.d | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/esvsearch.d b/esvsearch.d index d0dbcbc..24085ae 100644 --- a/esvsearch.d +++ b/esvsearch.d @@ -44,6 +44,7 @@ main(string[] args) ESVApi esv; string apiKey; string configPath; + string query; sharedInit(args); @@ -73,6 +74,8 @@ main(string[] args) return 1; } + query = args[1].dup(); + /* determine configuration file: options take first priority, * then environment variables, and then the default path */ configPath = environment.get(cf.configEnv, cf.configPath) @@ -100,7 +103,7 @@ main(string[] args) esv = ESVApi(apiKey); try - writeln(esv.searchFormat(args[1])); + writeln(esv.searchFormat(query)); catch (ESVException) die("no results"); catch (CurlException e) From 1d3340335583f1e26000542165201713c08415be Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Thu, 8 Aug 2024 17:12:04 +1200 Subject: [PATCH 115/133] esvsearch: add -e option for exact matches --- esvsearch.d | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/esvsearch.d b/esvsearch.d index 24085ae..17b1c24 100644 --- a/esvsearch.d +++ b/esvsearch.d @@ -35,6 +35,7 @@ import cf = config; @safe: string cFlag; /* config path */ +bool eFlag; /* exact matches */ bool VFlag; /* show version */ int @@ -57,6 +58,7 @@ main(string[] args) config.bundling, config.caseSensitive, "c", &cFlag, + "e", &eFlag, "V", &VFlag, ); } catch (GetOptException e) { @@ -75,6 +77,7 @@ main(string[] args) } query = args[1].dup(); + query = eFlag ? `"` ~ query ~ `"` : query; /* determine configuration file: options take first priority, * then environment variables, and then the default path */ @@ -102,6 +105,10 @@ main(string[] args) esv = ESVApi(apiKey); + foreach (char ch; args[1]) { + enforceDie(ch != '"', "query is invalid; remove any double quotes"); + } + try writeln(esv.searchFormat(query)); catch (ESVException) From dd51f65691467913ba587e340358d86488a3a1ac Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Thu, 8 Aug 2024 17:21:39 +1200 Subject: [PATCH 116/133] builds/nixos.yml: add LDC 1.30.0 build --- .builds/nixos.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.builds/nixos.yml b/.builds/nixos.yml index 98db6a0..485e514 100644 --- a/.builds/nixos.yml +++ b/.builds/nixos.yml @@ -18,6 +18,11 @@ tasks: cd esv ./configure -c ldc2 make + - build-ldc-1_30_0: | + nix build -o ldc-1.30.0 --accept-flake-config github:PetarKirov/dlang.nix#ldc-1_30_0 + cd esv + ./configure -c $(readlink ../ldc-1.30.0)/bin/ldc2 + make - install: | nix profile install ./esv - test: | From e8dc004ab9df5a7660a1657e120578dabde7ca24 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Thu, 8 Aug 2024 17:56:50 +1200 Subject: [PATCH 117/133] initial: re-vendor @ a3d17fd a3d17fd fix error with LDC 1.30.0: cannot create a `string[string]` with `new` f115e01 wrap lines --- initial.d | 41 +++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/initial.d b/initial.d index 289c33c..1843cec 100644 --- a/initial.d +++ b/initial.d @@ -3,27 +3,29 @@ * * Boost Software License - Version 1.0 - August 17th, 2003 * - * Permission is hereby granted, free of charge, to any person or organization - * obtaining a copy of the software and accompanying documentation covered by - * this license (the "Software") to use, reproduce, display, distribute, - * execute, and transmit the Software, and to prepare derivative works of the - * Software, and to permit third-parties to whom the Software is furnished to - * do so, all subject to the following: + * Permission is hereby granted, free of charge, to any person or + * organization obtaining a copy of the software and accompanying + * documentation covered by this license (the "Software") to use, + * reproduce, display, distribute, execute, and transmit the Software, + * and to prepare derivative works of the Software, and to permit + * third-parties to whom the Software is furnished to do so, all + * subject to the following: * - * The copyright notices in the Software and this entire statement, including - * the above license grant, this restriction and the following disclaimer, - * must be included in all copies of the Software, in whole or in part, and - * all derivative works of the Software, unless such copies or derivative - * works are solely in the form of machine-executable object code generated by - * a source language processor. + * The copyright notices in the Software and this entire statement, + * including the above license grant, this restriction and the following + * disclaimer, must be included in all copies of the Software, in whole or + * in part, and all derivative works of the Software, unless such copies or + * derivative works are solely in the form of machine-executable object + * code generated by a source language processor. * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT - * SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE - * FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, - * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - * DEALINGS IN THE SOFTWARE. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND + * NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR ANYONE + * DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR OTHER LIABILITY, + * WHETHER IN CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. */ /++ @@ -215,7 +217,6 @@ struct INISection this(string name) nothrow { this.name = name; - keys = new string[string]; } /++ From 13ba66d5679892a14ad2591f4d473e4e99042076 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Thu, 19 Sep 2024 12:41:01 +1200 Subject: [PATCH 118/133] esvsearch: update usage --- esvsearch.d | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esvsearch.d b/esvsearch.d index 17b1c24..9649b16 100644 --- a/esvsearch.d +++ b/esvsearch.d @@ -71,7 +71,7 @@ main(string[] args) } if (args.length < 2) { - stderr.writefln("usage: %s [-l length] query", + stderr.writefln("usage: %s [-e] [-l length] query", baseName(args[0])); return 1; } From 47045188fb43826970aa71a80ceb430b447165a2 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Sat, 4 Jan 2025 12:48:37 +1300 Subject: [PATCH 119/133] tree: update copyright to 2025 --- esv.d | 2 +- esvapi.d | 2 +- esvsearch.d | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/esv.d b/esv.d index 3db4888..39690c8 100644 --- a/esv.d +++ b/esv.d @@ -2,7 +2,7 @@ * esv: read the Bible from your terminal * * The GPLv2 License (GPLv2) - * Copyright (c) 2023-2024 Jeremy Baxter + * 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 diff --git a/esvapi.d b/esvapi.d index 294e1a6..78fa28c 100644 --- a/esvapi.d +++ b/esvapi.d @@ -2,7 +2,7 @@ * esvapi.d: a reusable interface to the ESV HTTP API * * The GPLv2 License (GPLv2) - * Copyright (c) 2023-2024 Jeremy Baxter + * 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 diff --git a/esvsearch.d b/esvsearch.d index 9649b16..4953e1a 100644 --- a/esvsearch.d +++ b/esvsearch.d @@ -2,7 +2,7 @@ * esvsearch: search the Bible from your terminal * * The GPLv2 License (GPLv2) - * Copyright (c) 2023-2024 Jeremy Baxter + * 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 From 06443cde4508daedf535942e30c257afccd0b19d Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Wed, 30 Apr 2025 14:03:50 +1200 Subject: [PATCH 120/133] builds: drop --- .builds/nixos.yml | 34 ---------------------------------- .builds/openbsd.yml | 21 --------------------- 2 files changed, 55 deletions(-) delete mode 100644 .builds/nixos.yml delete mode 100644 .builds/openbsd.yml diff --git a/.builds/nixos.yml b/.builds/nixos.yml deleted file mode 100644 index 485e514..0000000 --- a/.builds/nixos.yml +++ /dev/null @@ -1,34 +0,0 @@ ---- -image: nixos/unstable -packages: - - nixos.dmd - - nixos.curl - - nixos.gnumake - - nixos.ldc -environment: - NIX_CONFIG: "experimental-features = nix-command flakes" -sources: - - "https://git.sr.ht/~jeremy/esv" -tasks: - - build-dmd: | - cd esv - ./configure -c dmd - make all clean - - build-ldc: | - cd esv - ./configure -c ldc2 - make - - build-ldc-1_30_0: | - nix build -o ldc-1.30.0 --accept-flake-config github:PetarKirov/dlang.nix#ldc-1_30_0 - cd esv - ./configure -c $(readlink ../ldc-1.30.0)/bin/ldc2 - make - - install: | - nix profile install ./esv - - test: | - cd esv - ./configure -t - make clean all - ./esv - # very basic test :) - esv Matthew 5-7 >/dev/null diff --git a/.builds/openbsd.yml b/.builds/openbsd.yml deleted file mode 100644 index 59c1043..0000000 --- a/.builds/openbsd.yml +++ /dev/null @@ -1,21 +0,0 @@ ---- -image: openbsd/7.4 -packages: - - curl - - ldc -sources: - - "https://git.sr.ht/~jeremy/esv" -tasks: - - build: | - cd esv - ./configure - make - - install: | - doas make -C esv install - - test: | - cd esv - ./configure -t - make clean all - ./esv - # very basic test :) - esv Matthew 5-7 >/dev/null From 95ed1b41dd5c1e19bb941472f4a47382b77d261c Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Wed, 30 Apr 2025 22:40:03 +1200 Subject: [PATCH 121/133] README: add minimal readme --- README | 13 ++++++++++ README.md | 71 ------------------------------------------------------- 2 files changed, 13 insertions(+), 71 deletions(-) create mode 100644 README delete mode 100644 README.md diff --git a/README b/README new file mode 100644 index 0000000..c92b8b7 --- /dev/null +++ b/README @@ -0,0 +1,13 @@ +This is esv, a program that displays Bible passages on the terminal. + +To build, first install the LDC compiler and libcurl. +Configure the build environment, compile and optionally install: + + $ ./configure + $ make + # make install + +Documentation can be found in the man page esv(1). A quick start guide +is provided at the project's website . + +Written by Jeremy Baxter diff --git a/README.md b/README.md deleted file mode 100644 index 6cc1a3c..0000000 --- a/README.md +++ /dev/null @@ -1,71 +0,0 @@ -## esv - read the Bible from your terminal - -[![builds.sr.ht status](https://builds.sr.ht/~jeremy/esv.svg)](https://builds.sr.ht/~jeremy/esv) - -`esv` displays passages of the English Standard Version of the Bible on -your terminal. It connects to the ESV web API to retrieve the passages, -and allows configuration with command line options and the configuration -file. - -``` -$ esv Psalm 23 -Psalm 23 - -The LORD Is My Shepherd - -A Psalm of David. - - The LORD is my shepherd; I shall not want. - He makes me lie down in green pastures.... -``` - -The names of Bible books are case-insensitive, so John, john, -and JOHN are all accepted. - -## Audio - -esv supports playing audio passages through the -a flag. The -mpg123 audio player is used for playing audio by default but -you can set the `ESV_PLAYER` environment variable to something -else such as mpv if you don't have/don't want to use mpg123. - -Playing audio is mostly the same as reading a normal text passage. -`esv -a Matthew 5-7` will play an audio passage of Matthew 5-7. - -## Installation - -To install esv, first make sure you have a D compiler installed. -LDC is recommended but you can also use DMD. - -Commands prefixed with a dollar sign ($) are intended to be run as a -standard user, and commands prefixed with a hash sign (#) are intended -to be run as the root user. - -In [the list of releases][1] select the latest release, and download the -archive with the name esv-X.X.X.tar.gz. After that extract it: - - $ tar -xf esv-*.tar.gz - -Now, compile and install: - - $ cd esv-* - $ ./configure - $ make - # make install - -[1]: https://git.sr.ht/~jeremy/esv/refs - -## Documentation - -All documentation is contained in the manual pages. To access them, -you can run `man esv` and `man esv.conf` for the `esv` utility and the -configuration file respectively. - -## Copying - -Copying, modifying and redistributing this software is permitted -under the terms of the GNU General Public License version 2. - -Full license texts are contained in the COPYING file. - -Copyright (c) 2023-2024 Jeremy Baxter From 0fe81de6c217417a5503c8f6b12eb704592f3d27 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Wed, 30 Apr 2025 22:40:16 +1200 Subject: [PATCH 122/133] esvsearch: change message displayed on -V --- esvsearch.d | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esvsearch.d b/esvsearch.d index 4953e1a..e990d73 100644 --- a/esvsearch.d +++ b/esvsearch.d @@ -66,7 +66,7 @@ main(string[] args) } if (VFlag) { - writeln("esvsearch " ~ cf.esvVersion); + writeln("esvsearch from esv " ~ cf.esvVersion); return 0; } From d7201d9f9aba12806fd13fbcfaeef3384dff81fc Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Wed, 30 Apr 2025 22:40:52 +1200 Subject: [PATCH 123/133] esvsearch: update inaccurate usage message --- esvsearch.d | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esvsearch.d b/esvsearch.d index e990d73..f65990e 100644 --- a/esvsearch.d +++ b/esvsearch.d @@ -71,7 +71,7 @@ main(string[] args) } if (args.length < 2) { - stderr.writefln("usage: %s [-e] [-l length] query", + stderr.writefln("usage: %s [-eV] [-c config] query", baseName(args[0])); return 1; } From e8da3e35cf343f0358ce389a094cea31e777336e Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Thu, 1 May 2025 14:00:10 +1200 Subject: [PATCH 124/133] esv.1, esvsearch.1: add website URL --- esv.1 | 18 ++++++++++-------- esvsearch.1 | 14 ++++++++------ 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/esv.1 b/esv.1 index d8b58c1..305bdf9 100644 --- a/esv.1 +++ b/esv.1 @@ -1,4 +1,4 @@ -.Dd $Mdocdate: June 26 2024 $ +.Dd $Mdocdate: May 01 2025 $ .Dt ESV 1 .Os .Sh NAME @@ -143,12 +143,14 @@ Search the Bible for the phrase .Sh SEE ALSO .Xr esv.conf 5 .Sh AUTHORS -.An Jeremy Baxter Aq Mt jeremy@baxters.nz +.An Jeremy Baxter Aq Mt jeremy@reformers.dev +.Pp +Part of the +.Sy esv +distribution found at +.Lk https://reformers.dev/esv .Sh BUGS Currently there are no known bugs in -.Nm ; -but if you think you've found a potential bug, -please report it to me using my email address above. -.Pp -Existing bugs and planned features can be found on the bug tracker: -.Lk https://todo.sr.ht/~jeremy/esv +.Nm . +If you think you've found a potential bug, +please report it to my email address above. diff --git a/esvsearch.1 b/esvsearch.1 index c8a1046..c2a3c83 100644 --- a/esvsearch.1 +++ b/esvsearch.1 @@ -1,4 +1,4 @@ -.Dd $Mdocdate: June 26 2024 $ +.Dd $Mdocdate: May 01 2025 $ .Dt ESVSEARCH 1 .Os .Sh NAME @@ -55,7 +55,12 @@ Search the Bible for verses containing .Xr esv 1 , .Xr esv.conf 5 .Sh AUTHORS -.An Jeremy Baxter Aq Mt jeremy@baxters.nz +.An Jeremy Baxter Aq Mt jeremy@reformers.dev +.Pp +Part of the +.Sy esv +distribution found at +.Lk https://reformers.dev/esv .Sh BUGS .Nm outputs search results in a human-readable manner, @@ -66,7 +71,4 @@ should support outputting results in a shell-readable format and the JSON format. .Pp If you think you've found a potential bug, -please report it to me using my email address above. -.Pp -Existing bugs and planned features can be found on the bug tracker: -.Lk https://todo.sr.ht/~jeremy/esv +please report it to my email address above. From 94a954d6345310237219a3c18366345648b2e3ab Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Thu, 1 May 2025 14:00:37 +1200 Subject: [PATCH 125/133] esvsearch.1: document -e option --- esvsearch.1 | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/esvsearch.1 b/esvsearch.1 index c2a3c83..0b8ca4e 100644 --- a/esvsearch.1 +++ b/esvsearch.1 @@ -7,7 +7,7 @@ .Sh SYNOPSIS .Nm esvsearch .Bk -words -.Op Fl V +.Op Fl eV .Op Fl c Ar config .Ar query .Ek @@ -41,7 +41,10 @@ See the documentation on in .Xr esv 1 for more information. -.Sx ENVIRONMENT ) . +.It Fl e +Instead of showing loose matches for +.Ar query , +only show exact matches. .It Fl V Print the version number and exit. .El From 3d483fd0f46aa766710044384a6ec9d1d01fd556 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Thu, 1 May 2025 14:41:00 +1200 Subject: [PATCH 126/133] esvapi: modify searchFormat() to take a function --- esvapi.d | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/esvapi.d b/esvapi.d index 78fa28c..fd6fb11 100644 --- a/esvapi.d +++ b/esvapi.d @@ -178,6 +178,12 @@ verseValid(in char[] verse) assert(verseValid("15:12-17")); } +string +defaultSearchFmt(string reference, string content) pure +{ + return format!"\033[1m%s\033[0m\n %s\n"(reference, content.wrap(80)); +} + /++ + Structure containing the authentication key, API URL, + any parameters to use when making a request as well as the @@ -319,11 +325,12 @@ struct ESVApi } /++ - + Calls search() and formats the results nicely as plain text. + + Calls search() and formats the results nicely as plain text, + + unless a custom function is provided. +/ string - searchFormat(alias fmt = "\033[1m%s\033[0m\n %s\n") - (in string query, int lineLength = 0) /* 0 means default */ + searchFormat(in string query, + string function(string, string) fmt = &defaultSearchFmt) { char[] layout; JSONValue resp; @@ -334,15 +341,9 @@ struct ESVApi enforce!ESVException(resp["total"].integer != 0, "No results for search"); - lineLength = lineLength == 0 ? 80 : lineLength; - () @trusted { foreach (JSONValue item; resp["results"].array) { - layout ~= format!fmt( - item["reference"].str, - item["content"].str - .wrap(lineLength) - ); + layout ~= fmt(item["reference"].str, item["content"].str); } }(); From 4564b88e4254d436f5386d3839e6891034dd015a Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Thu, 1 May 2025 19:12:40 +1200 Subject: [PATCH 127/133] esvapi: rename ESVAPI_PARAMETERS to esvapiParameters Keep with D code style --- esvapi.d | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esvapi.d b/esvapi.d index fd6fb11..aa3f05a 100644 --- a/esvapi.d +++ b/esvapi.d @@ -114,7 +114,7 @@ immutable string[] bibleBooks = [ ]; /++ All allowed API parameters (for text passages). +/ -immutable string[] ESVAPI_PARAMETERS = [ +immutable string[] esvapiParameters = [ "include-passage-references", "include-verse-numbers", "include-first-verse-numbers", From 67171b71dc71043004215898d2b306dc8760a3c9 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Thu, 1 May 2025 19:36:02 +1200 Subject: [PATCH 128/133] esvsearch: implement machine-readable output format --- esvsearch.d | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/esvsearch.d b/esvsearch.d index f65990e..c070a9b 100644 --- a/esvsearch.d +++ b/esvsearch.d @@ -24,7 +24,9 @@ import std.file : FileException; import std.getopt : getopt, GetOptException; import std.path : baseName, expandTilde; import std.process : environment; +import std.regex : regex, matchAll, replaceFirst; import std.stdio : writeln, writefln; +import std.string : tr; import esvapi; import initial; @@ -36,8 +38,28 @@ import cf = config; string cFlag; /* config path */ bool eFlag; /* exact matches */ +bool mFlag; /* machine readable */ bool VFlag; /* show version */ +string +machineReadableFmt(string reference, string content) +{ + /* match the start of the reference against bibleBooks + * to identify what book it's from, so we can replace + * spaces in the book name with underscores :-) */ + foreach (string book; bibleBooks) { + auto match = reference.matchAll(regex("^(" ~ book ~ ") \\d")); + if (!match.empty) { + assert(match.captures[1] == book + && bookValid(match.captures[1])); + reference = reference.replaceFirst( + regex('^' ~ book), book.tr(" ", "_")); + } + } + + return reference ~ " / " ~ content ~ "\n"; +} + int main(string[] args) { @@ -59,6 +81,7 @@ main(string[] args) config.caseSensitive, "c", &cFlag, "e", &eFlag, + "m", &mFlag, "V", &VFlag, ); } catch (GetOptException e) { @@ -71,7 +94,7 @@ main(string[] args) } if (args.length < 2) { - stderr.writefln("usage: %s [-eV] [-c config] query", + stderr.writefln("usage: %s [-emV] [-c config] query", baseName(args[0])); return 1; } @@ -110,7 +133,9 @@ main(string[] args) } try - writeln(esv.searchFormat(query)); + writeln(mFlag + ? esv.searchFormat(query, &machineReadableFmt) + : esv.searchFormat(query)); catch (ESVException) die("no results"); catch (CurlException e) From bc342d0415c05f81670467fed7b3922ee5669d92 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Thu, 1 May 2025 19:42:57 +1200 Subject: [PATCH 129/133] esvsearch.1: document -m --- esvsearch.1 | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/esvsearch.1 b/esvsearch.1 index 0b8ca4e..50bf4b0 100644 --- a/esvsearch.1 +++ b/esvsearch.1 @@ -7,7 +7,7 @@ .Sh SYNOPSIS .Nm esvsearch .Bk -words -.Op Fl eV +.Op Fl emV .Op Fl c Ar config .Ar query .Ek @@ -45,6 +45,12 @@ for more information. Instead of showing loose matches for .Ar query , only show exact matches. +.It Fl m +Print matches in a machine-readable format, +where each result takes up just one line. +Any spaces in the book name are replaced with underscores +and a slash character separates the passage reference +from the passage content. .It Fl V Print the version number and exit. .El @@ -65,13 +71,7 @@ Part of the distribution found at .Lk https://reformers.dev/esv .Sh BUGS -.Nm -outputs search results in a human-readable manner, -which makes it nice for humans to read but difficult -for machines to parse and store. -.Nm -should support outputting results in a shell-readable format -and the JSON format. -.Pp +Currently there are no known bugs in +.Nm . If you think you've found a potential bug, please report it to my email address above. From 5c366d64b0bf2f78e235d5aaa1f6d02a8af08422 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Thu, 1 May 2025 20:00:24 +1200 Subject: [PATCH 130/133] esv.1: revise --- esv.1 | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/esv.1 b/esv.1 index 305bdf9..30c5a51 100644 --- a/esv.1 +++ b/esv.1 @@ -15,8 +15,7 @@ .Sh DESCRIPTION .Nm displays Bible passages on your terminal. -It can also play recorded audio tracks of certain passages, -through integration with an MP3 player utility. +It can also play audio passages. .Pp See the section .Sx EXAMPLES @@ -27,29 +26,28 @@ Verses can be provided in the format of .Em chapter:verse , and .Em chapter:verse-verse . -If the name of your desired book has a space in it, e.g. +If the name of your desired book has spaces in it, e.g. .Dq "1 Corinthians" , -you can put a hyphen or underscore in the place of the space, -or you can just pass the full book name with the space in it -by surrounding the argument with quotes in your shell. +you can provide the book name with hyphens or underscores in place of +the spaces, or you can pass the original book name. Thus, both .Dq 1-Corinthians and -.Dq "1 Corinthians" -are valid book names. +.Dq 1_Corinthians +are also valid book names. .Pp By default, .Xr mpg123 1 -is used as the MP3 player. -However, this can be overridden; +is used as the player for audio passages. +This can be overridden however; see the .Sx ENVIRONMENT -section for more information on this. +section for more information. .Pp The options are as follows: .Bl -tag -width 123456 .It Fl a -Play a recorded audio track rather than showing a passage. +Play an audio passage instead of printing a text passage. .It Fl c Ar config Read the configuration from the path .Ar config . @@ -114,7 +112,8 @@ Where to read the configuration file, rather than using the default location (see section .Sx FILES ) . .It Ev ESV_PLAYER -The name of the MP3 player to use for playing audio passages. +The name of the audio player to use when playing audio passages. +The program specified must support playing MP3 audio. If this is not set, .Nm will look for @@ -133,14 +132,10 @@ Read John 1:29-31: .Pp Listen to a recorded audio track of Psalm 128: .Pp -.Dl esv -a Psalm 128 -.Pp -Search the Bible for the phrase -.Dq "in love" : -.Pp -.Dl esv -s 'in love' +.Dl esv -a Psalm 139 .Pp .Sh SEE ALSO +.Xr esvsearch 1 .Xr esv.conf 5 .Sh AUTHORS .An Jeremy Baxter Aq Mt jeremy@reformers.dev From 0b2133bae2050d99f86232a439de9af982994ec7 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Thu, 1 May 2025 20:07:35 +1200 Subject: [PATCH 131/133] esv.conf.5: revise --- esv.conf.5 | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/esv.conf.5 b/esv.conf.5 index 34164c3..0612b8d 100644 --- a/esv.conf.5 +++ b/esv.conf.5 @@ -1,4 +1,4 @@ -.Dd $Mdocdate: March 23 2023 $ +.Dd $Mdocdate: May 01 2025 $ .Dt ESV.CONF 5 .Os .Sh NAME @@ -15,9 +15,12 @@ An example is listed below: .Dl [section] .Dl key = value .Pp -Comments can be used by putting a hashtag +A line beginning with a hashtag .Dq # -symbol at the beginning of a line. +will be considered a +.Dq comment +and will be ignored by +.Xr esv 1 . .Pp The available configuration options are as follows: .Bl -tag -width keyword From 1136fa01bbcf469f62b5b97ddd59def9aae587a3 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Thu, 1 May 2025 20:09:59 +1200 Subject: [PATCH 132/133] esv-0.3.0 --- README | 2 +- config.di | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README b/README index c92b8b7..1429a8d 100644 --- a/README +++ b/README @@ -1,4 +1,4 @@ -This is esv, a program that displays Bible passages on the terminal. +This is esv 0.3.0, a program that displays Bible passages on the terminal. To build, first install the LDC compiler and libcurl. Configure the build environment, compile and optionally install: diff --git a/config.di b/config.di index 041e561..5a15fdc 100644 --- a/config.di +++ b/config.di @@ -2,7 +2,7 @@ module config; public: -enum esvVersion = "0.2.0-dev"; +enum esvVersion = "0.3.0"; enum apiKey = "abfb7456fa52ec4292c79e435890cfa3df14dc2b"; enum configPath = "~/.config/esv.conf"; From e3665cc5d79a09eea149d8a2edf1d20bbe9f6fdd Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Tue, 19 Aug 2025 12:45:30 +1200 Subject: [PATCH 133/133] correct email --- README | 2 +- esv.1 | 2 +- esvsearch.1 | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README b/README index 1429a8d..abb6e1d 100644 --- a/README +++ b/README @@ -10,4 +10,4 @@ Configure the build environment, compile and optionally install: Documentation can be found in the man page esv(1). A quick start guide is provided at the project's website . -Written by Jeremy Baxter +Written by Jeremy Baxter diff --git a/esv.1 b/esv.1 index 30c5a51..bb02e6d 100644 --- a/esv.1 +++ b/esv.1 @@ -138,7 +138,7 @@ Listen to a recorded audio track of Psalm 128: .Xr esvsearch 1 .Xr esv.conf 5 .Sh AUTHORS -.An Jeremy Baxter Aq Mt jeremy@reformers.dev +.An Jeremy Baxter Aq Mt jeremy@baxters.nz .Pp Part of the .Sy esv diff --git a/esvsearch.1 b/esvsearch.1 index 50bf4b0..8fba334 100644 --- a/esvsearch.1 +++ b/esvsearch.1 @@ -64,7 +64,7 @@ Search the Bible for verses containing .Xr esv 1 , .Xr esv.conf 5 .Sh AUTHORS -.An Jeremy Baxter Aq Mt jeremy@reformers.dev +.An Jeremy Baxter Aq Mt jeremy@baxters.nz .Pp Part of the .Sy esv