// 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 . // system #include #include #include #include #include #include // third-party #include // 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; }