scriptura/scriptura.cpp

668 lines
18 KiB
C++

// Copyright 2021, Paul Mosier
//
// This file is part of Scriptura, a ncurses-based Bible study
// software for the libsword backend.
//
// Scriptura 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, under version 2 of the License.
//
// Scriptura 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 Scriptura. If not, see <https://www.gnu.org/licenses/>.
// system
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ncurses.h>
#include <unistd.h>
#include <locale.h>
// third-party
#include <swconfig.h>
// mine
#include "free.h"
#include "pane.h"
#include "scabbard.h"
/* --[ ASSISTS / TOP NAMESPACE ]-- */
// quick array for page names
char* pages[PAGELIMIT][NUMPANES];
int currentpage, totalpages;
int halfcols, halflines;
int floatx, floaty, floatheight, floatwidth;
int startx1, starty1, startx2, starty2;
int panelength, panewidth;
sword::SWConfig config;
//! Write background text in stdscr
void foundation() {
// wipe out old page info line
for (int i = 0; i < COLS - 20; i++) mvaddch(LINES - 1, i, ' ');
// place page info & background text
int placement = 0;
for (int i = 0; i < totalpages; i++) {
// NOTE -- we are assuming the length of a searchkey & size of NUMPANES
if (i == currentpage) attron(A_STANDOUT);
mvprintw(LINES - 1, placement, "[%d]:%s/%s", i,
config[pages[i][0]]["searchkey"].c_str(),
config[pages[i][1]]["searchkey"].c_str());
attroff(A_STANDOUT);
placement += 26;
}
mvprintw(LINES - 1 , COLS - 20, "%s", "? - help q - quit");
refresh();
}
//! Refresh every portion of a list of panes we are given.
void wipeit(pane* panes) {
clear();
foundation();
for (int i = 0; i < NUMPANES; i++) panes[i].redraw(true);
}
//! Display a form field.
starray showForm(const char* title, starray inputs, const char* secondinst,
int floaty, int floatx, int floatheight, int floatwidth) {
// pull up the floating form
modkey defmod;
defmod.searchkey = NULL;
pane form = {floaty, floatx, floatheight, floatwidth, title, defmod};
starray ret = form.loadForm(inputs, secondinst);
return ret;
}
//! Construct a modkey from the currently selected page & pane
modkey getModkey(int infocus) {
modkey mod;
mod.modname = config[pages[currentpage][infocus]]["module"];
mod.searchkey = config[pages[currentpage][infocus]]["searchkey"];
mod.searchtype = parseConf(config[pages[currentpage][infocus]]["searchtype"]);
mod.keytype = parseConf(config[pages[currentpage][infocus]]["keytype"]);
mod.scope = config[pages[currentpage][infocus]]["scope"];
return mod;
}
//! Refresh to the current page.
void loadPage(pane* panes, scabbard* scab) {
for (int i = 0; i < NUMPANES; i++) {
modkey mod = getModkey(i);
panes[i].setModkey(mod, scab->getModDescription(mod.modname));
// searchtype will tell us whether this is a search or a straight lookup
if (mod.searchtype == 1) {
panes[i].loadText(scab->getSpan(mod));
} else {
panes[i].loadText(scab->search(mod));
}
}
wipeit(panes);
}
//! Save the config array -- we abstract this out in case this changes.
void saveit() {
config.save();
}
//! Set various standard screen landmarks.
void landmarks() {
foundation();
// split up the screen
halfcols = COLS / 2;
halflines = LINES / 2;
// floating window measurements
floatx = 3;
floaty = 3;
floatheight = LINES - 6;
floatwidth = COLS - 6;
// define our main windows and their borders
if (! strcmp(config["layout"]["panels"], "v")) {
// vertical layout
startx1 = starty1 = startx2 = 0;
starty2 = halflines;
panelength = LINES - halflines - 1;
panewidth = COLS;
} else {
// horizontal layout
startx1 = starty1 = starty2 = 0;
startx2 = halfcols;
panelength = LINES - 1;
panewidth = COLS - halfcols;
}
}
//! Resize the standard screen.
void doresize(pane* p) {
// terminal window was resized
clear();
// reset standard screen landmarks
landmarks();
// reset and refresh main panes -- assumes NUMPANES
p[0].resize(starty1, startx1, panelength, panewidth);
p[1].resize(starty2, startx2, panelength, panewidth);
wipeit(p);
}
/* --[ MAIN ]-- */
int main(int argc, char** argv) {
// if help argument supplied, give text & exit
if ((argc > 1) &&
((! strcmp(argv[1], "-h")) || (! strcmp(argv[1], "--help")))) {
printf("%s\n", CLITEXT);
exit(0);
}
// read in our configuration file, or whatever the user supplies
char* homedir = getenv("HOME");
if (homedir == NULL) {
printf("No home directory, pilgrim.\n");
perror("(Could not find $HOME.)\n");
return -1;
}
char* configfile;
asprintf(&configfile, "%s%s", homedir, "/.config/scriptura.ini");
if ((argc > 2) && (! strcmp(argv[1], "-c"))) {
free(configfile);
configfile = argv[2];
if (access(configfile, R_OK|W_OK) == -1) {
perror("Error loading supplied configuration file");
return -1;
}
} else {
// configfile remains default
if (access(configfile, R_OK|W_OK) == -1) {
printf("Creating new configuration file...");
FILE *cnf = fopen(configfile, "w");
if (cnf == NULL) {
printf("\nCould not create new config file $HOME/.config/scriptura.ini\n");
printf("Please ensure this directory exists and is writable.\n");
return -1;
}
fclose(cnf);
printf("done.\n");
}
}
config = *(new sword::SWConfig(configfile));
/* load up a scabbard & set blank starting data - scabbard args are
* sanitized in the constructor */
scabbard scab = {};
/* before we render anything, ensure some needed config settings are in
* place & sanitized, with needed changes saved back to file */
if (! strcmp(config["layout"]["panels"], ""))
config["layout"]["panels"] = "v";
if (! strcmp(config["defaults"]["scope"], ""))
config["defaults"]["scope"] = "Gen 1:1 - Rev 22:21";
if (! strcmp(config["defaults"]["searchkey"], ""))
config["defaults"]["searchkey"] = "2 Tim 1:7";
if ((! strcmp(config["defaults"]["module"], "")) ||
(! scab.modExists(config["defaults"]["module"]))) {
/* prompt for a default module - we're on partial startup here and
* still outside of ncurses, so we do this the old fashioned way. */
printf("You either do not have a default module selected, "
"or what you have chosen is not installed.\n");
printf("Please select one before continuing.\n\n");
printf("0 - Install KJVA from ftp.crosswire.org.\n");
starray mods = scab.getModules();
for (int i = 0; i < mods.length; i++) {
const char* longname = scab.getModDescription(mods.strings[i]);
printf("%d - %s\n", i+1, longname);
}
printf("\nDefault module to choose: ");
char readit[4];
fgets(readit, 4, stdin);
long selection = 0;
selection = strtol(readit, NULL, 10);
const char* defmod;
if ((selection == 0) || (selection > mods.length)) {
printf("Installing from ftp.crosswire.org...\n");
scab.installKJVA();
defmod = "KJVA";
printf("done.\n");
} else defmod = mods.strings[selection - 1];
config["defaults"]["module"] = defmod;
}
if (! strcmp(config["defaults"]["searchtype"], "")) {
char* temp;
asprintf(&temp, "%d", DEFSEARCH);
config["defaults"]["searchtype"] = temp;
free(temp);
}
if (! strcmp(config["page0-0"]["searchkey"], ""))
config["page0-0"]["searchkey"] = "2 Tim 1:7";
if (! strcmp(config["page0-1"]["searchkey"], ""))
config["page0-1"]["searchkey"] = "2 Tim 1:7";
saveit();
// NOTE -- anything after this point must use our defined exit wrapup()
/* start ncurses, disable line buffering hide cursor, and allow for fancy
* keys & so on */
setlocale(LC_ALL, "");
initscr();
cbreak();
curs_set(0);
noecho();
start_color();
keypad(stdscr, true);
// get color settings
if (! strcmp(config["markup"]["lettercolor"], "green")) {
init_pair(1, COLOR_GREEN, COLOR_BLACK);
} else if (! strcmp(config["markup"]["lettercolor"], "yellow")) {
init_pair(1, COLOR_YELLOW, COLOR_BLACK);
} else if (! strcmp(config["markup"]["lettercolor"], "magenta")) {
init_pair(1, COLOR_MAGENTA, COLOR_BLACK);
} else if (! strcmp(config["markup"]["lettercolor"], "blue")) {
init_pair(1, COLOR_BLUE, COLOR_BLACK);
} else if (! strcmp(config["markup"]["lettercolor"], "cyan")) {
init_pair(1, COLOR_CYAN, COLOR_BLACK);
} else {
init_pair(1, COLOR_RED, COLOR_BLACK);
}
// cycle through config file for any saved pages & sanitize
totalpages = 0;
int linking = 1;
for (int i = 0; i < PAGELIMIT; i++) {
for (int j = 0; j < NUMPANES; j++) {
/* sanitize input here --
* It's our own config but the user can edit it. If a searchkey doesn't
* exist then we need to stop to ensure the list is linked properly.
* If it does, accept whatever the searchkey is, even if the user
* modified it. For other settings, verify they are in the file, or take
* the defaults & update the config. When we get to the first blank
* searchkey for the start of a page, no longer take any further input. */
asprintf(&(pages[i][j]), "page%d%s%d", i, "-", j);
/* with no searchkey, if this is the first pane then stop linking here;
* otherwise we can get by with setting the searchkey to the default */
if (! strcmp(config[pages[i][j]]["searchkey"], "")) {
(j == 0 ? linking = 0 :
config[pages[i][j]]["searchkey"] = config["defaults"]["searchkey"]);
}
if (linking && ((! strcmp(config[pages[i][j]]["module"], "")) ||
(! scab.modExists(config[pages[i][j]]["module"])))) {
config[pages[i][j]]["module"] = config["defaults"]["module"];
}
if (linking && (! strcmp(config[pages[i][j]]["keytype"], "")))
config[pages[i][j]]["keytype"] = config["defaults"]["keytype"];
if (linking && (! strcmp(config[pages[i][j]]["searchtype"], "")))
config[pages[i][j]]["searchtype"] = config["defaults"]["searchtype"];
if (linking && (! strcmp(config[pages[i][j]]["scope"], "")))
config[pages[i][j]]["scope"] = config["defaults"]["scope"];
// we have a page
if (linking && (j == 0)) totalpages++;
}
}
// set index for page list
currentpage = 0;
// initialize panes and render
landmarks();
pane* p = (pane*) malloc(sizeof(pane) * NUMPANES);
if (! p) wrapup(1, "Error allocating memory for panes.\n");
for (int i = 0; i < NUMPANES; i++) {
modkey mod = getModkey(i);
// NOTE -- assumes NUMPANES, for future work
int starty, startx;
starty = (i == 0 ? starty1 : starty2);
startx = (i == 0 ? startx1 : startx2);
p[i] = { starty, startx, panelength, panewidth,
scab.getModDescription(mod.modname), mod };
}
pane* focustab;
int infocus = 0;
focustab = &(p[infocus]);
focustab->toggleFocus();
loadPage(p, &scab);
// loop for input
int ch;
while ((ch = getch()) != 'q') {
switch (ch) {
case '\t': {
// switch pane focus
focustab->toggleFocus();
infocus = (infocus + 1) % NUMPANES;
focustab = &(p[infocus]);
focustab->toggleFocus();
break; }
case KEY_NPAGE:
focustab->nextPage();
break;
case KEY_DOWN:
focustab->scrollDown();
break;
case KEY_UP:
focustab->scrollUp();
break;
case KEY_PPAGE:
focustab->prevPage();
break;
case KEY_RESIZE:
doresize(p);
break;
case '0':
case '1':
case '2':
case '3':
case '4':
// go to page
if (totalpages <= (int) ch - 48) break;
currentpage = (int) ch - 48;
loadPage(p, &scab);
break;
case '<': {
// previous page
currentpage = (currentpage == 0 ? totalpages - 1 : currentpage - 1);
loadPage(p, &scab);
break; }
case '>': {
// next page
currentpage = (currentpage + 1) % totalpages;
loadPage(p, &scab);
break; }
case 'd': {
// delete page
if (totalpages == 1) break;
for (int i = currentpage; i < totalpages; i++) {
for (int j = 0; j < NUMPANES; j++) {
if (i == totalpages - 1) {
// delete the last in list
config[pages[i][j]]["searchkey"] = NULL;
} else {
// shift all later pages down to fill the gap
config[pages[i][j]]["module"] = config[pages[i+1][j]]["module"];
config[pages[i][j]]["searchkey"] =
config[pages[i+1][j]]["searchkey"];
config[pages[i][j]]["searchtype"] =
config[pages[i+1][j]]["searchtype"];
config[pages[i][j]]["keytype"] = config[pages[i+1][j]]["keytype"];
config[pages[i][j]]["scope"] = config[pages[i+1][j]]["scope"];
}
}
}
totalpages--;
// reset page index if needed to stay in bounds
if (currentpage == totalpages) currentpage--;
loadPage(p, &scab);
saveit();
break; }
case 'f': {
// toggle footnotes
int footnotes = ! parseConf(config["markup"]["footnotes"]);
config["markup"]["footnotes"] = footnotes + '0';
saveit();
break; }
case 'g': {
// go to a particular passage
starray goinst;
goinst = stinit(goinst);
goinst = stappend(goinst, "Go to: ");
const char* gotitle = "Go to passage";
const char* secondinst = "Enter passage to retrieve; "
"abbreviations & disjoint passage selections are okay.";
starray ret = showForm(gotitle, goinst, secondinst, floaty,
floatx, floatheight, floatwidth);
if (ret.length == -1) {
doresize(p);
break;
}
if ((ret.length != 0) && (strlen((ret.strings)[0]) > 0)) {
// user entered something that's not a blank string
config[pages[currentpage][infocus]]["searchkey"] = (ret.strings)[0];
focustab->setKey((ret.strings)[0]);
focustab->loadText(scab.getSpan(getModkey(infocus)));
saveit();
}
wipeit(p);
break; }
case 'n': {
// toggle non-textual words
int textual = ! parseConf(config["markup"]["nontextual"]);
config["markup"]["nontextual"] = textual + '0';
saveit();
break; }
case 'o': {
// load a module into the current pane
const char* typetitle = "Choose a module type";
modkey defmod;
defmod.searchkey = NULL;
pane menupane = {floaty, floatx, floatheight, floatwidth,
typetitle, defmod };
starray mc = scab.getModClassifications();
int modclass = menupane.loadMenu(mc);
if (modclass == -1) {
wipeit(p);
break;
} else if (modclass == -2) {
doresize(p);
break;
}
const char* modtitle = "Choose a module";
menupane = {floaty, floatx, floatheight, floatwidth, modtitle, defmod};
starray mds = scab.getModDescriptions(modclass);
int mod = menupane.loadMenu(mds);
if (mod == -1) {
wipeit(p);
break;
} else if (mod == -2) {
doresize(p);
break;
}
config[pages[currentpage][infocus]]["module"] =
scab.getModName(modclass, mod);
char* temp;
asprintf(&temp, "%d", scab.getKeyType(modclass));
config[pages[currentpage][infocus]]["keytype"] = temp;
free(temp);
focustab->setModule(scab.getModDescription(modclass, mod),
scab.getModName(modclass, mod),
scab.getKeyType(modclass));
focustab->loadText(scab.getSpan(getModkey(infocus)));
saveit();
wipeit(p);
break; }
case 'p': {
// new page
if (totalpages == PAGELIMIT) break;
for (int j = 0; j < NUMPANES; j++) {
config[pages[totalpages][j]]["module"] = config["defaults"]["module"].c_str();
config[pages[totalpages][j]]["searchkey"] = config["defaults"]["searchkey"].c_str();
config[pages[totalpages][j]]["scope"] = config["defaults"]["scope"].c_str();
char* temp;
asprintf(&temp, "%d", DEFKEY);
config[pages[totalpages][j]]["keytype"] = temp;
free(temp);
asprintf(&temp, "%d", DEFSEARCH);
config[pages[totalpages][j]]["searchtype"] = temp;
free(temp);
}
currentpage = totalpages;
totalpages++;
saveit();
loadPage(p, &scab);
break; }
case 'r': {
// toggle redletter
int redletter = ! parseConf(config["markup"]["redletters"]);
config["markup"]["redletters"] = redletter + '0';
saveit();
break; }
case 's': {
// search
starray inst;
inst = stinit(inst);
/* NOTE -- these happen to be in the same order as the
* integer key used by Sword to determine the search type,
* and this may be subject to breakage if those integer values
* change */
inst = stappend(inst, "Search for this word or part:");
inst = stappend(inst, "Search for this phrase:");
inst = stappend(inst, "Search for multiple words:");
inst = stappend(inst, "Search for attribute (eg Strongs#):");
inst = stappend(inst, "SPACE");
inst = stappend(inst, "Restrict search to this text:");
const char* title = "Search";
const char* secondinst = "Enter your search on the correct line"
" and specify the scope if needed.";
starray ret = showForm(title, inst, secondinst, floaty,
floatx, floatheight, floatwidth);
if (ret.length == -1) {
doresize(p);
break;
}
if (ret.length != 0) {
/* user entered something - get first field with data and
* the search scope (the last item), if any */
int i = 0;
for (i = 0; i < ret.length - 1; i++) {
if (strcmp((ret.strings)[i], "") != 0) {
// ensure we have a search scope
const char* newscope;
if (strcmp((ret.strings)[ret.length-1], "") != 0) {
newscope = (ret.strings)[ret.length-1];
} else {
newscope = config["defaults"]["scope"];
}
// update both pane modkey & config file to keep consistency
config[pages[currentpage][infocus]]["searchkey"] = (ret.strings)[i];
config[pages[currentpage][infocus]]["scope"] = newscope;
char* temp;
asprintf(&temp, "%d", i * -1);
config[pages[currentpage][infocus]]["searchtype"] = temp;
free(temp);
focustab->setKey((ret.strings)[i]);
focustab->setSearch(i * -1, newscope);
break;
}
}
if (i != ret.length - 1) {
focustab->loadText(scab.search(getModkey(infocus)));
saveit();
}
}
wipeit(p);
break; }
case 't': {
// toggle Strong's numbers
int strongs = ! parseConf(config["markup"]["strongs"]);
config["markup"]["strongs"] = strongs + '0';
saveit();
break; }
case 'w': {
// toggle raw text
int raw = ! parseConf(config["markup"]["rawtext"]);
config["markup"]["rawtext"] = raw + '0';
saveit();
break; }
case '?': {
// display help text
clear();
int x, y;
int i = 0;
while (HELPTEXT[i] != '\0') {
addch(HELPTEXT[i++]);
getyx(stdscr, y, x);
if (y == LINES - 1) {
// pay attention to terminal size
getch();
clear();
}
}
getch();
wipeit(p);
break; }
case 'q':
// quit - we shouldn't get here
break;
} // switch
} // input loop
// go away cleanly
wrapup(0, NULL);
// shouldn't get here
return 0;
}