#include #include #include #include #include #include #include "support.h" #include "TodoDB.h" #include "Strings.h" #include "CommandArgs.h" #include "todoterm.h" #include "config.h" #include "todorl.h" using namespace str; static bool nukeonhit = false; Options options; Options::Options() : verbose(0), purgeage(0), mono(false), paranoid(false), global(false), summary(false), timeout(false), comment(false), database(".todo"), priority(Todo::None), mode(TodoDB::View), backups(0), timeoutseconds(0) { // default formatting strings format["date"] = "%c"; format["display"] = "%4>%i%f%[info]%2n.%[priority]%+1T\n"; format["comment-display"] = "%4>%i%f%[info]%2n.%[priority]%+1T\n%+1i%[comment](Comment: %C)\n"; format["verbose-display"] = "%i%[info]%f%2n.%[priority]%+1T\n%+1i%[info]Added: %[normal]%c %[info]Completed: %[normal]%d\n%+1i%[info]Duration: %[normal]%D %[info]Priority: %[normal]%p\n\n"; format["generated"] = "%2>%i-%+1T\n%+1i(added %c, %d, priority %p)\n\n"; format["verbose-generated"] = "%2>%i- %+1T\n\n%+1iAdded: %c Completed: %d\n%+1iDuration: %D Priority: %p\n\n\n"; loaders.push_back("xml"); loaders.push_back("binary"); if (getenv("HOME")) globaldatabase = string(getenv("HOME")) + "/.todo_global"; else globaldatabase = ""; filename = ""; columns = getWidth(); } Options::Filter::Filter() { // defaults for filter childrendir = Equal; children = false; prioritydir = Equal; priority = Todo::None; donedir = Negative; done = true; show = Equal; } void parseSort(string const &arg) { vector keys = str::split(",", arg); struct { const char *str; int key; } table[] = { { "created", Options::Sort::Created }, { "completed", Options::Sort::Completed }, { "text", Options::Sort::Text }, { "priority", Options::Sort::Priority }, { "duration", Options::Sort::Duration }, { "done", Options::Sort::Done }, { "none", Options::Sort::None }, { 0, 0 } }; options.sort.clear(); for (vector::iterator i = keys.begin(); i != keys.end(); ++i) { string p = *i; Options::Sort sort; if (p[0] == '-') { sort.dir = Options::Negative; p = p.substr(1); } else { sort.dir = Options::Equal; if (p[0] == '+') p = p.substr(1); } int j; for (j = 0; table[j].str != 0; ++j) if (p == table[j].str) { sort.key = (Options::Sort::Key)table[j].key; break; } if (table[j].str == 0) throw runtime_error("unknown sort key '" + p + "'"); options.sort.push_back(sort); } } void parseFilter(TodoDB &todo, string const &arg) { vector out; out = split(",", arg); for (vector::iterator i = out.begin(); i != out.end(); i++) { Options::Dir dir = Options::Equal; string str = *i; // Search if (str[0] == '/') { options.filter.search = str.c_str() + 1; } else { if (str[0] == '-' || str[0] == '+' || str[0] == '=') { if (str[0] == '-') dir = Options::Negative; else if (str[0] == '+') dir = Options::Positive; else dir = Options::Equal; // singular '-' or '+' sets default display mode if (str.size() == 1) { options.filter.show = dir; continue; } str = str.substr(1); } // must be children? if (str == "children") { options.filter.children = true; options.filter.childrendir = dir; } else // ...or done? if (str == "done") { options.filter.done = true; options.filter.donedir = dir; } else // catch-all, show everything if (str == "all") { options.filter.done = true; options.filter.donedir = Options::Positive; options.filter.children = true; options.filter.childrendir = Options::Positive; } else if (isdigit(str[0])) { vector indices = todo.getIndexList(str); for (vector::iterator i = indices.begin(); i != indices.end(); ++i) { options.filter.item[*i] = dir; if (dir != Options::Negative) options.filter.show = Options::Negative; } } else // is it a priority? try { Todo::Priority n = desymbolisePriority(symbolisePriority(str)); options.filter.priority = n; options.filter.prioritydir = dir; } catch (logic_error&) { throw runtime_error("unexpected symbol '" + str + "' in filter expression"); } } } } static void mergeVectors(vector &out, vector const &in) { for (vector::const_iterator i = in.begin(); i != in.end(); ++i) out.push_back(*i); } /** Parse command line arguments into an associative array of key/value pairs. @param argc argc from main() @param argv argv from main() @return key/value pairs */ void parseArgs(TodoDB &todo, int argc, char const **argv) { CommandArgs args; string myname = argv[0]; // detect default mode from command name if (myname.rfind("tda") == myname.size() - 3) options.mode = TodoDB::Add; if (myname.rfind("tde") == myname.size() - 3) options.mode = TodoDB::Edit; if (myname.rfind("tdr") == myname.size() - 3) options.mode = TodoDB::Remove; if (myname.rfind("tdd") == myname.size() - 3) options.mode = TodoDB::Done; if (myname.rfind("tdl") == myname.size() - 3) options.mode = TodoDB::Link; enum { Help = -100, Remove, Version, Title, Colour, Mono, ForceColour, Database, GlobalDatabase, DateFormat, DisplayFormat, VerboseDisplayFormat, VerboseGeneratedFormat, GeneratedFormat, Sort, Paranoid, DatabaseLoaders, Backup, Timeout, Format, UseFormat, DumpConfig, Exec, Echo, Stats, Purge }; args.addArgument('a', "add", CommandArgs::Optional); args.setHelp('a', "Add a note (will prompt for a note if one is not supplied)."); args.addArgument('l', "link", CommandArgs::Required); args.setHelp('l', "Links another todo file to this one."); args.addArgument('g', "graft", CommandArgs::Required); args.setHelp('g', "In conjunction with --add or --link, graft the new item to the specified item."); args.addArgument('R', "reparent", CommandArgs::Required); args.setHelp('R', "Change the parent of the first item index to the second item index (seperated by a comma). If no second index is given the item is reparented to the root of the tree. eg. todo -R 1,2"); args.addArgument('p', "priority", CommandArgs::Required); args.setHelp('p', "In conjunction with --add, set the default priority (default|veryhigh|high|medium|low|verylow)."); args.addArgument('e', "edit", CommandArgs::Required); args.setHelp('e', "Edit the note that is indexed by the given number."); args.addArgument(Remove, "remove", CommandArgs::Required); args.setHelp(Remove, "Remove the note that is indexed by the given number."); args.addArgument('d', "done", CommandArgs::Required); args.setHelp('d', "Mark the specified notes (and all children) as done."); args.addArgument('c', "comment"); args.setHelp('c', "Also manipulate finishing comment for item when viewing or marking as done."); args.addArgument('D', "not-done", CommandArgs::Required); args.setHelp('D', "Mark the specified notes (and all children) as not done."); args.addArgument(GlobalDatabase, "global-database", CommandArgs::Required); args.setHelp(GlobalDatabase, "Specify the database to use if the -G (--global) parameter is used."); args.addArgument('G', "global"); args.setHelp('G', "Use the database specified by the --global-database option. Defaults to ~/.todo_global."); args.addArgument(Database, "database", CommandArgs::Required); args.setHelp(Database, "Change the database from the default (.todo) to the filename specified."); args.addArgument('T', "TODO"); args.setHelp('T', "Generate a typical TODO output text file from a Todo DB."); args.addArgument('A', "all"); args.setHelp('A', "Shortcut for the filter '+done,+children' to show all notes."); args.addArgument('f', "filter", CommandArgs::Required); args.setHelp('f', "Display only those notes that pass the filter. A filter is composed of the minimum or maximum priority level, the keywords 'done', 'all' or 'children'. Alternatively, a regular expression search filter can be specified with '/'. eg. '-low,=done,-children' would show any notes with a priority less than or equal to 'low' that are marked as done and would not show child notes."); args.addArgument('v', "verbose"); args.setHelp('v', "Display verbosely."); args.addArgument(Colour, "colour", CommandArgs::Required); args.setHelp(Colour, "Override default colours of todo items. Multiple colours can be specified, seperated by commas. eg. high=red,medium=white. Items are bolded by prefixing them with a '+'."); args.addArgument(Mono, "mono"); args.setHelp(Mono, "Disable all use of colour."); args.addArgument(ForceColour, "force-colour"); args.setHelp(ForceColour, "Force use of colour."); args.addArgument(Help, "help"); args.setHelp(Help, "Display this help."); args.addArgument(Version, "version"); args.setHelp(Version, "Display version of todo."); args.addArgument(Title, "title", CommandArgs::Optional); args.setHelp(Title, "Set the title of this directory's todo notes."); args.addArgument(DateFormat, "date-format", CommandArgs::Required); args.setHelp(DateFormat, "Format the display of time values. The format is that used by strftime(3)."); args.addArgument(Format, "format", CommandArgs::Required); args.setHelp(Format, "Specify a display format to use. ARG is in the form =."); args.addArgument(UseFormat, "use-format", CommandArgs::Required); args.setHelp(UseFormat, "Map one format string to another (eg. --use-format display=my-own-format)."); args.addArgument(DumpConfig, "dump-config"); // Individual formatting strings are deprecated args.addArgument(DisplayFormat, "display-format", CommandArgs::Required); //args.setHelp(DisplayFormat, "Specify the format of items when displaying them to the console."); args.addArgument(VerboseDisplayFormat, "verbose-display-format", CommandArgs::Required); //args.setHelp(VerboseDisplayFormat, "As with --display-format but used when --verbose (-v) is specified."); args.addArgument(GeneratedFormat, "generated-format", CommandArgs::Required); //args.setHelp(GeneratedFormat, "Specify the format of items when generating a TODO file from the database."); args.addArgument(VerboseGeneratedFormat, "verbose-generated-format", CommandArgs::Required); //args.setHelp(VerboseGeneratedFormat, "As with --generated-format but used when --verbose (-v) is specified."); args.addArgument(Sort, "sort", CommandArgs::Required); args.setHelp(Sort, "Sort the database with the specified expression."); args.addArgument(Paranoid, "paranoid"); args.setHelp(Paranoid, "Be paranoid about some settings, including permissions."); args.addArgument(DatabaseLoaders, "database-loaders", CommandArgs::Required); args.setHelp(DatabaseLoaders, "Specify order of database format loaders to use when loading and saving (defaults to 'xml')."); args.addArgument(Backup, "backup", CommandArgs::Optional); args.setHelp(Backup, "Enable backups of the database. The optional argument is the number of backups to keep (defaults to 1)."); args.addArgument('s', "summary"); args.setHelp('s', "Toggle summary mode."); args.addArgument(Timeout, "timeout", CommandArgs::Optional); args.setHelp(Timeout, "Set the number of seconds between database displays and/or conform to this setting (see man page for more information)."); args.addArgument(Purge, "purge", CommandArgs::Optional); args.setHelp(Purge, "Purge items marked as done. Optionally only purge completed items older than ARG days."); args.addArgument(Exec, "exec", CommandArgs::Required); args.addArgument(Echo, "echo", CommandArgs::Required); args.addArgument(Stats, "stats"); for (CommandArgs::iterator i = args.begin(argc, argv); !(i == args.end()); i++) switch (i.option()) { case Help : cout << "usage: " << myname << " []" << endl << "Where can be any of the following:" << endl; args.displayHelp(cout, options.columns); cout << endl << "In addition, there are five convenience symlinks. These are 'tda', 'tdr'," << endl << "'tdd', 'tde', and 'tdl'. For 'tde', 'tdd' and 'tdr' supply an index to edit," << endl << "mark done and remove respectively. For 'tda' supply the text of the todo item" << endl << "item and optionally the priority. For 'tdl' supply the path to another todo" << endl << "file to link in to the current todo file." << endl << "eg. tde 1" << endl; exit(0); break; case Version : cout << VERSION << endl; exit(0); break; case Title : if (i.parameter()) options.text = i.parameter(); options.mode = TodoDB::Title; break; case DateFormat : options.format["date"] = i.parameter(); break; case Mono : options.mono = true; break; case Database : options.database = i.parameter(); break; case Sort : parseSort(i.parameter()); break; case GlobalDatabase : options.globaldatabase = i.parameter(); break; case Backup : if (i.parameter()) { options.backups = destringify(i.parameter()); if (options.backups <= 0) throw runtime_error("can't specify backup revisions <= 0"); } else options.backups = 1; break; case Timeout : if (i.parameter()) { options.timeoutseconds = destringify(i.parameter()); } else { if (options.timeoutseconds == 0) options.timeoutseconds = 10; options.timeout = true; } break; case Purge : if (i.parameter()) options.purgeage = destringify(i.parameter()); options.mode = TodoDB::Purge; break; case Exec : if (options.verbose > 1) cout << "todo: executing '" << i.parameter() << "'" << endl; system(i.parameter()); break; case Echo : cout << i.parameter() << endl; break; case 's' : options.summary = !options.summary; break; case 'G' : options.global = true; break; case Paranoid : options.paranoid = true; break; case DatabaseLoaders : options.loaders = str::split(",", i.parameter()); break; case Colour : { vector colours = str::split(",", i.parameter()); for (vector::iterator j = colours.begin(); j != colours.end(); ++j) { vector component = str::split("=", *j); if (component.size() != 2) throw runtime_error(string("colour values should be in the format =, not '") + i.parameter() + "'"); todo.setColour(component[0], component[1]); } } break; case ForceColour : term::forceColour(true); break; case Format : { vector comp; if (!strchr(i.parameter(), '=')) throw runtime_error("invalid usage of --format"); string tmp = i.parameter(); comp.push_back(tmp.substr(0, strchr(i.parameter(), '=') - i.parameter())); comp.push_back(tmp.substr(strchr(i.parameter(), '=') - i.parameter() + 1)); options.format[comp[0]] = comp[1]; } break; case UseFormat : { vector mapping = str::split("=", i.parameter()); if (mapping.size() != 2) throw runtime_error("invalid usage of --use-format"); if (options.format.find(mapping[1]) == options.format.end() || options.format.find(mapping[1]) == options.format.end()) throw runtime_error("no such format '" + mapping[1] + "'"); options.format[mapping[0]] = options.format[mapping[1]]; } break; case DumpConfig : { cout << "# todo version " << VERSION << " config dump" << endl; cout << "# terminal columns at time of dump: " << options.columns << endl; { vector temp; cout << "filter "; if (options.filter.priority != Todo::None) { string s; if (options.filter.prioritydir == Options::Negative) s = "-"; else if (options.filter.prioritydir == Options::Positive) s = "+"; switch (options.filter.priority) { case Todo::VeryLow : s += "verylow"; break; case Todo::Low : s += "low"; break; case Todo::Medium : s += "medium"; break; case Todo::High : s += "high"; break; case Todo::VeryHigh : s += "veryhigh"; break; default : break; } temp.push_back(s); } if (options.filter.children) if (options.filter.childrendir == Options::Negative) temp.push_back("-children"); else if (options.filter.childrendir == Options::Positive) temp.push_back("+children"); else temp.push_back("children"); if (options.filter.done) if (options.filter.donedir == Options::Negative) temp.push_back("-done"); else if (options.filter.donedir == Options::Positive) temp.push_back("+done"); else temp.push_back("done"); if (options.filter.show == Options::Negative) temp.push_back("-"); else if (options.filter.show == Options::Positive) temp.push_back("+"); if (options.filter.search.source() != "") temp.push_back("/" + options.filter.search.source()); cout << str::join(",", temp) << endl; } cout << "sort "; for (vector::iterator i = options.sort.begin(); i != options.sort.end();) { if ((*i).dir == Options::Negative) cout << '-'; if ((*i).dir == Options::Positive) cout << '+'; switch ((*i).key) { case Options::Sort::None : cout << "none"; break; case Options::Sort::Created : cout << "created"; break; case Options::Sort::Completed : cout << "completed"; break; case Options::Sort::Text : cout << "text"; break; case Options::Sort::Priority : cout << "priority"; break; case Options::Sort::Duration : cout << "duration"; break; case Options::Sort::Done : cout << "done"; break; } ++i; if (i != options.sort.end()) cout << ","; } cout << endl; for (map::iterator i = options.format.begin(); i != options.format.end(); ++i) cout << "format " << (*i).first << "=" << str::addcslashes((*i).second) << endl; if (options.priority != Todo::None) cout << "priority " << symbolisePriority(options.priority) << endl; if (options.verbose) cout << "verbose" << endl; if (options.mono) cout << "mono" << endl; if (options.paranoid) cout << "paranoid" << endl; if (options.summary) cout << "summary" << endl; if (options.timeoutseconds) cout << "timeout " << options.timeoutseconds << endl; if (options.text != "") cout << "# user supplied text is '" << options.text << "'" << endl; cout << "database " << options.database << endl; if (options.global) cout << "# using global database..." << endl; cout << "global-database " << options.globaldatabase << endl; if (options.backups) cout << "backup " << options.backups << endl; if (options.index.size()) cout << "# user supplied indices " << str::join(",", options.index) << endl; cout << "database-loaders " << str::join(",", options.loaders) << endl; for (map >::iterator i = options.event.begin(); i != options.event.end(); ++i) { cout << "on " << (*i).first << " "; cout << str::join(" ", (*i).second).substr(2) << endl; } exit(0); } break; case VerboseDisplayFormat : options.format["verbose-display"] = i.parameter(); break; case VerboseGeneratedFormat : options.format["verbose-generated"] = i.parameter(); break; case DisplayFormat : options.format["display"] = i.parameter(); break; case GeneratedFormat : options.format["generated"] = i.parameter(); break; case Stats : options.mode = TodoDB::Stats; break; case 'v' : options.verbose++; break; case 'a' : // optional text to add if (i.parameter()) options.text = i.parameter(); options.mode = TodoDB::Add; break; case 'l' : if (i.parameter()) options.filename = i.parameter(); options.mode = TodoDB::Link; break; case 'g' : options.index = todo.getIndexList(i.parameter()); if (options.index.size() > 1) cerr << "warning: more than one note index given in graft, ignoring tail" << endl; break; case 'R' : options.index = todo.getIndexList(i.parameter()); if (options.index.size() > 2) throw runtime_error("--reparent accepts a maximum of two comma seperated indices"); options.mode = TodoDB::Reparent; break; case 'p' : options.priority = desymbolisePriority(symbolisePriority(i.parameter())); break; case 'e' : options.index = todo.getIndexList(i.parameter()); if (options.index.size() > 1) cerr << "warning: more than one note index given in edit, ignoring tail" << endl; options.mode = TodoDB::Edit; break; case Remove : options.index = todo.getIndexList(i.parameter()); options.mode = TodoDB::Remove; break; case 'c' : options.comment = true; break; case 'd' : options.index = todo.getIndexList(i.parameter()); options.mode = TodoDB::Done; break; case 'D' : options.index = todo.getIndexList(i.parameter()); options.mode = TodoDB::NotDone; break; case 'T' : options.mode = TodoDB::Generate; break; case 'A' : options.filter.children = true; options.filter.childrendir = Options::Positive; options.filter.done = true; options.filter.donedir = Options::Positive; break; case 'f' : parseFilter(todo, i.parameter()); break; case 0 : // default parameter for tda is the todo text body if (options.mode == TodoDB::Add) { if (options.text != "") options.text += " " + string(i.parameter()); else options.text = i.parameter(); continue; } // default paramter for tdr is the index if (options.mode == TodoDB::Remove) { mergeVectors(options.index, todo.getIndexList(i.parameter())); continue; } // default paramter for tdd is the index if (options.mode == TodoDB::Done) { mergeVectors(options.index, todo.getIndexList(i.parameter())); continue; } // default parameter for tde is the index if (options.mode == TodoDB::Edit) { mergeVectors(options.index, todo.getIndexList(i.parameter())); continue; } // default parameter for tdl is the path to a todo file if (options.mode == TodoDB::Link) { options.filename = i.parameter(); continue; } // default parameters for viewing are filters if (options.mode == TodoDB::View) { try { parseFilter(todo, i.parameter()); continue; } catch (...) { } } throw invalid_argument(string("unknown argument '") + i.parameter() + "', try --help"); break; } } string symbolisePriority(string sym) { static struct { char const *s, *n; } s[] = { { "default", "-2" }, { "verylow", "5" }, { "low", "4" }, { "medium", "3" }, { "high", "2" }, { "veryhigh", "1" }, { 0, 0 }, }; for (int i = 0; s[i].s; i++) if (sym == s[i].s || sym == s[i].n) return s[i].s; throw logic_error("unknown priority level '" + sym + "'"); } string symbolisePriority(Todo::Priority sym) { static struct { char const *s; Todo::Priority p; } s[] = { { "default", Todo::Default }, { "verylow", Todo::VeryLow }, { "low", Todo::Low }, { "medium", Todo::Medium }, { "high", Todo::High }, { "veryhigh", Todo::VeryHigh }, { 0, Todo::None }, }; for (int i = 0; s[i].s; i++) if (sym == s[i].p) return s[i].s; throw logic_error("unknown priority level '" + stringify(sym) + "'"); } Todo::Priority desymbolisePriority(string sym) { static struct { char const *s, *n; Todo::Priority p; } s[] = { { "default", "-2", Todo::Default }, { "verylow", "5", Todo::VeryLow }, { "low", "4", Todo::Low }, { "medium", "3", Todo::Medium }, { "high", "2", Todo::High }, { "veryhigh", "1", Todo::VeryHigh }, { 0, 0, Todo::None }, }; sym = trim(sym); for (int i = 0; s[i].s; i++) if (sym == s[i].s || sym == s[i].n) return s[i].p; throw logic_error("unknown symbolic priority level '" + sym + "'"); } time_t getCurrentDate() { return time(0); } string dateToHuman(time_t date) { // If we have strftime, use it #ifdef HAVE_STRFTIME char buffer[64]; strftime(buffer, 64, options.format["date"].c_str(), localtime(&date)); return buffer; #else string out = ctime(&date); return out.substr(0, out.size() - 1); #endif } /* Readline support */ string const *rl_buffer; static char *getMatches(char *text, int state) { static char const *list[] = { "veryhigh", "high", "medium", "low", "verylow", 0 }; static int i, len; char const *t; if (!state) { i = 0; len = strlen(text); } while ((t = list[i]) != 0) { ++i; if (!strncmp(t, text, len)) return strdup(t); } return 0; } static char **completion(char *text, int start, int end) { if (start == 0) return completion_matches(text, (char*(*)(...))getMatches); return 0; } static int init_rl() { rl_insert_text(const_cast(rl_buffer->c_str())); rl_attempted_completion_function = (char **(*)(...))completion; return 0; } static int nuke_on_hit() { int c = rl_getc(stdin); if (nukeonhit && c != 13) { rl_delete_text(0, rl_end); rl_end = rl_mark = rl_point = 0; rl_redisplay(); rl_clear_message(); nukeonhit = false; } if (c == 14) { rl_insert_text(const_cast("\n")); return 0; } return c; } string readText(string const &prompt, string existing, bool nuke) { string out; rl_startup_hook = (int(*)(...))init_rl; rl_getc_function = (int(*)())nuke_on_hit; nukeonhit = nuke; rl_buffer = &existing; char const *tmp = readline(const_cast(prompt.c_str())); if (tmp) out = tmp; return out; } void addHistory(string text) { add_history(const_cast(text.c_str())); } static bool rcvalid(string const &str) { return str == "verbose" || str == "filter" || str == "priority" || str == "TODO" || str == "colour" || str == "mono" || str == "global-database" || str == "global" || str == "database" || str == "date-format" || str == "sort" || str == "display-format" || str == "generated-format" || str == "verbose-display-format" || str == "verbose-generated-format" || str == "database-loaders" || str == "backup" || str == "summary" || str == "timeoutseconds" || str == "timeout" || str == "format" || str == "use-format" || str == "paranoid" || str == "exec" || str == "echo" || str == "comment" || str == "force-colour"; } /* Order of RC file parsing: $TODORC, falling back to SYSCONFDIR/todorc $HOME/.todorc */ vector parseRC() { vector rcfile, out; // $TODORC if (getenv("TODORC")) { ifstream test(getenv("TODORC")); if (test.bad()) cerr << "warning: TODORC envar points to invalid file '" << getenv("TODORC") << "'" << endl; else rcfile.push_back(getenv("TODORC")); } else { // SYSCONFDIR/todorc (typically /etc/todorc) ifstream test(SYSCONFDIR "/todorc"); if (test.good()) rcfile.push_back(SYSCONFDIR "/todorc"); } // $HOME/.todorc if (getenv("HOME")) { ifstream test((string(getenv("HOME")) + "/.todorc").c_str()); if (test.good()) rcfile.push_back((string(getenv("HOME")) + "/.todorc").c_str()); } if (!rcfile.size()) return out; // Load config information from all RC files for (vector::iterator rc = rcfile.begin(); rc != rcfile.end(); ++rc) { ifstream in((*rc).c_str()); char inputline[1024], *line = inputline; while (in.getline(line, 1024)) { string str; istringstream is(line); if (is >> str) { if (str[0] == '#') continue; // Parse events if (str == "on") { string event; vector data; is >> event; if (event != "add" && event != "remove" && event != "view" && event != "edit" && event != "generate" && event != "done" && event != "done" && event != "notdone" && event != "title" && event != "reparent" && event != "create" && event != "save" && event != "load" && event != "link") throw runtime_error(string("unknown event '") + event + "'"); is >> str; // multi-line event ? if (str == "{") { while (in.getline(line, 1024)) { istringstream is(line); if (is >> str) { if (str[0] == '#') continue; if (str[0] == '}') break; if (rcvalid(str)) { data.push_back("--" + str); if (getline(is, str)) { string remainder = str::trim(str); if (remainder != "") data.push_back(remainder); } } } } } else if (rcvalid(str)) { data.push_back("--" + str); if (getline(is, str)) { string remainder = str::trim(str); if (remainder != "") data.push_back(remainder); } } else throw runtime_error(("unrecognised resource '" + str + "' in " + *rc).c_str()); options.event[event] = data; } else if (rcvalid(str)) { // these are the only valid options in the rc file out.push_back("--" + str); string remainder = expandEnvars(str::trim(line + str.size())); if (remainder != "") out.push_back(remainder); } else throw runtime_error(("unrecognised resource '" + str + "' in " + *rc).c_str()); } } } return out; } string expandEnvars(string const &str) { string out; for (unsigned i = 0; i < str.size(); ++i) if (str[i] == '$') { i++; unsigned mark = i; for (; str[i] != 0 && (isalnum(str[i]) || str[i] == '_'); ++i) ; if (i != mark) { string var = str.substr(mark, i - mark); if (!getenv(var.c_str())) { if (options.verbose) cout << "notice: no such environment variable '" + var + "'." << endl; } else out += getenv(var.c_str()); } else out += '$'; i--; } else out += str[i]; return out; } string elapsedToHuman(time_t start, time_t end) { time_t t = end - start; string ret; if (t < 0) return "?? (-ve)"; if (t == 0) return "No time"; if (t / (60 * 60 * 24 * 7)) ret += stringify(t / (60 * 60 * 24 * 7)) + "w "; if (t / (60 * 60 * 24)) ret += stringify((t / (60 * 60 * 24)) % 7) + "d "; if (t / (60 * 60)) ret += stringify((t / (60 * 60)) % 24) + "h "; if (t / 60) ret += stringify((t / 60) % 60) + "m "; //ret += stringify(t % 60) + "s"; return ret; }