"Fossies" - the Fresh Open Source Software Archive

Member "devtodo-0.1.20/src/TodoDB.cc" (7 Sep 2007, 41893 Bytes) of package /linux/privat/old/devtodo-0.1.20.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) C and C++ source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. For more information about "TodoDB.cc" see the Fossies "Dox" file reference documentation.

    1 
    2 // We don't want this freed by the delete function.
    3 #include <map>
    4 #include "Strings.h"
    5 #include "TodoDB.h"
    6 #include "Loaders.h"
    7 #include "support.h"
    8 #include "config.h"
    9 
   10 using namespace term;
   11 using namespace str;
   12 
   13 static bool coloursInitialised = false;
   14 
   15 map<string, TodoDB::StreamColour> TodoDB::streamColour;
   16 
   17 TodoDB::TodoDB() {
   18     if (!coloursInitialised) {
   19         initColour();
   20         coloursInitialised = true;
   21     }
   22 }
   23 
   24 TodoDB::TodoDB(string const &file) {
   25     initColour();
   26     load(file);
   27 }
   28 
   29 TodoDB::~TodoDB() {
   30 }
   31 
   32 void TodoDB::initColour() {
   33     streamColour["veryhigh"] = StreamColour(red, bold);
   34     streamColour["high"] = StreamColour(yellow, bold);
   35     streamColour["medium"] = StreamColour(::normal, StreamColour::mono);
   36     streamColour["low"] = StreamColour(cyan, StreamColour::mono);
   37     streamColour["verylow"] = StreamColour(blue, bold);
   38     streamColour["info"] = StreamColour(green, StreamColour::mono);
   39     streamColour["title"] = StreamColour(green, bold);
   40     streamColour["comment"] = StreamColour(white, bold);
   41 }
   42 
   43 void TodoDB::initColourPost() {
   44     if (options.mono) {
   45         streamColour["veryhigh"] = StreamColour(StreamColour::mono, StreamColour::mono);
   46         streamColour["high"] = StreamColour(StreamColour::mono, StreamColour::mono);
   47         streamColour["medium"] = StreamColour(StreamColour::mono, StreamColour::mono);
   48         streamColour["low"] = StreamColour(StreamColour::mono, StreamColour::mono);
   49         streamColour["verylow"] = StreamColour(StreamColour::mono, StreamColour::mono);
   50         streamColour["info"] = StreamColour(StreamColour::mono, StreamColour::mono);
   51         streamColour["title"] = StreamColour(StreamColour::mono, StreamColour::mono);
   52         streamColour["comment"] = StreamColour(StreamColour::mono, StreamColour::mono);
   53         priority[0] = StreamColour::mono;
   54         priority[1] = StreamColour::mono;
   55         priority[2] = StreamColour::mono;
   56         priority[3] = StreamColour::mono;
   57         priority[4] = StreamColour::mono;
   58         info = StreamColour::mono;
   59         title = StreamColour::mono;
   60         normal = StreamColour::mono;
   61         comment = StreamColour::mono;
   62     } else {
   63         priority[0] = StreamColour::veryhigh;
   64         priority[1] = StreamColour::high;
   65         priority[2] = StreamColour::medium;
   66         priority[3] = StreamColour::low;
   67         priority[4] = StreamColour::verylow;
   68         info = StreamColour::info;
   69         title = StreamColour::title;
   70         normal = StreamColour::normal;
   71         comment = StreamColour::comment;
   72     }
   73 
   74 }
   75 
   76 void TodoDB::operator () (Mode mode) {
   77     initColourPost();
   78     switch (mode) {
   79         case Add : triggerEvent("add"); add(); break;
   80         case Link : triggerEvent("link"); link(); break;
   81         case Remove : triggerEvent("remove"); remove(); break;
   82         case View : triggerEvent("view"); view(); break;
   83         case Edit : triggerEvent("edit"); edit(); break;
   84         case Generate : triggerEvent("generate"); generate(); break;
   85         case Done : triggerEvent("done"); done(); break;
   86         case NotDone : triggerEvent("notdone"); notdone(); break;
   87         case Title : triggerEvent("title"); edittitle(); break;
   88         case Reparent : triggerEvent("reparent"); reparent(); break;
   89         case Purge : triggerEvent("purge"); purge(); break;
   90         default :
   91             throw exception("unknown action?");
   92         break;
   93     }
   94 }
   95 
   96 /*  Find an item.
   97 
   98     Items are specified by their number. Sub-items are specified by a .
   99     followed by their number, and so on. The kleene start can be used to
  100     match any item at a level although wildcard matching does not actually
  101     occur here, but in the getIndexList method.
  102 */
  103 Todo *TodoDB::find(multiset<Todo> const &todo, string const &index) {
  104 int looking = destringify<int>(index);
  105 
  106     for (multiset<Todo>::const_iterator i = todo.begin(); i != todo.end(); i++)
  107         if ((*i).index == looking) {
  108             // Recurse into child
  109             if (index.find(".") != string::npos) {
  110                 try {
  111                 string ns = index.substr(index.find(".") + 1);
  112 
  113                     return find(*i->child, ns);
  114                 } catch (exception &e) {
  115                     throw exception("couldn't find index '" + index + "'");
  116                 }
  117             }
  118             return const_cast<Todo*>(&(*i));
  119         }
  120     throw exception("couldn't find index '" + index + "'");
  121 }
  122 
  123 multiset<Todo> &TodoDB::findContainer(multiset<Todo> &todo, string const &index) {
  124 int looking = destringify<int>(index);
  125 
  126     for (multiset<Todo>::iterator i = todo.begin(); i != todo.end(); i++)
  127         if ((*i).index == looking) {
  128             // Recurse into child
  129             if (index.find(".") != string::npos) {
  130                 try {
  131                 string ns = index.substr(index.find(".") + 1);
  132 
  133                     return findContainer(const_cast<multiset<Todo>& >(*i->child), ns);
  134                 } catch (exception &e) {
  135                     throw exception("couldn't find index '" + index + "'");
  136                 }
  137             }
  138             return todo;
  139         }
  140     throw exception("couldn't find index '" + index + "'");
  141 }
  142 
  143 //  Erase the item with the specified index.
  144 void TodoDB::erase(multiset<Todo> &todo, string const &index) {
  145 int looking;
  146 
  147     looking = destringify<int>(index);
  148     for (multiset<Todo>::iterator i = todo.begin(); i != todo.end(); i++)
  149         if ((*i).index == looking) {
  150             // Recurse into child
  151             if (index.find(".") != string::npos) {
  152                 try {
  153                 string ss = index.substr(index.find(".") + 1);
  154                     erase(*const_cast<Todo&>(*i).child, ss);
  155                 } catch (exception &e) {
  156                     throw exception("couldn't find index '" + index + "'");
  157                 }
  158             } else {
  159                 todo.erase(i);
  160                 setDirty(true);
  161             }
  162             return;
  163         }
  164     throw exception("couldn't find index '" + index + "'");
  165 }
  166 
  167 void TodoDB::edittitle() {
  168 string text;
  169 
  170     if (options.text != "") text = options.text;
  171     else {
  172         if (isatty(0)) {
  173             if (options.verbose)
  174                 cout << info << "Enter text for the title of this todo list." << normal << endl;
  175             text = readText("text> ", titleText);
  176         } else {
  177         string line;
  178 
  179             while (getline(cin, line))
  180                 text += line + '\n';
  181         }
  182     }
  183     titleText = text;
  184 
  185     setDirty(true);
  186 }
  187 
  188 void TodoDB::parse(vector<XML*>::const_iterator begin, 
  189     vector<XML*>::const_iterator end, multiset<Todo> &out) {
  190     for (vector<XML*>::const_iterator i = begin; i != end; i++) {
  191     XML &x = *(*i);
  192 
  193         switch (x.type()) {
  194             case XML::Element : {
  195                 if (x.name() == "title") {
  196                     titleText = trim((*x.child().begin())->body());
  197                     break;
  198                 } else
  199                 if (x.name() == "note") {
  200                 Todo todo;
  201                 // const_cast so I can use attrib[...]
  202                 map<string, string> &attrib = *const_cast<map<string, string>* >(&x.attrib());
  203 
  204                     if (attrib.find("priority") == attrib.end() || attrib.find("time") == attrib.end())
  205                         throw exception("require both 'priority' and 'time' attributes for 'note' element");
  206 
  207                     if (attrib.find("done") != attrib.end()) {
  208                             todo.done = true;
  209                             todo.doneTime = destringify<time_t>(attrib["done"]);
  210                         } else
  211                             todo.done = false;
  212 
  213                     todo.priority = desymbolisePriority(attrib["priority"]);
  214                     todo.text = trim((*x.child().begin())->body());
  215                     todo.added = destringify<time_t>(attrib["time"]);
  216 
  217                     parse(x.child().begin(), x.child().end(), *todo.child);
  218 
  219                     out.insert(todo);
  220                 } else if (x.name() == "link") {
  221                 TodoDB newDb;
  222                 Todo todo;
  223                 map<string, string> &attrib = *const_cast<map<string, string>* >(&x.attrib());
  224 
  225                     if (attrib.find("filename") == attrib.end())
  226                         throw exception("require 'filename' attribute for 'link' element");
  227 
  228                     newDb.load(attrib["filename"]);
  229                     
  230                     if (newDb.titleText == "")
  231                         todo.text = attrib["filename"];
  232                     else
  233                         todo.text = newDb.titleText;
  234 
  235                     todo.child = &newDb.todo;
  236 
  237                     out.insert(todo);
  238                 } else
  239                     throw exception("expected 'note' element, got '" + x.name() + "'");
  240             }
  241             break;
  242             default :
  243             break;
  244         }
  245     }
  246 
  247     // number the items
  248 int n = 1;
  249     for (multiset<Todo>::iterator i = out.begin(); i != out.end(); ++i, ++n) {
  250     Todo &t = const_cast<Todo&>(*i);
  251         
  252         t.index = n;
  253     }
  254 }
  255 
  256 void TodoDB::fixParents(multiset<Todo> &todo, Todo *parent) {
  257     for (multiset<Todo>::iterator i = todo.begin(); i != todo.end(); ++i) {
  258     Todo &todo = (Todo&)*i;
  259 
  260         if (parent)
  261             todo.parent = parent;
  262         fixParents(*todo.child, &todo);
  263     }
  264 }
  265 
  266 void TodoDB::load(string const &file) {
  267 Loader loader;
  268 string lastError;
  269 
  270     setDirty(false);
  271 
  272     stat(file.c_str(), &_stat);
  273     if (options.timeout && options.mode == View && (time(0) - _stat.st_atime < options.timeoutseconds)) {
  274         if (options.verbose)
  275             cout << "todo: database not displayed due to timeout" << endl;
  276         return;
  277     }
  278 
  279     options.timeout = false;
  280 
  281     filename = file;
  282 
  283     /* Get the base name */
  284 string::size_type pos = filename.rfind("/");
  285     
  286     if (pos > 0 && pos != string::npos)
  287         basepath = filename.substr(0, pos);
  288 
  289 ifstream in(file.c_str());
  290 
  291     statSuccessful = false;
  292     if (in.bad()||in.fail()||in.eof()) throw quit();
  293     statSuccessful = true;
  294 
  295     in.close();
  296 
  297     loader = getLoaders();
  298 
  299     for (vector<string>::iterator i = options.loaders.begin(); i != options.loaders.end(); ++i) {
  300         if (options.verbose > 1)
  301             cout << "todo: trying '" << (*i) << "' loader" << endl;
  302         if (loader.find(*i) == loader.end())
  303             throw exception("couldn't find loader for '" + *i + "'");
  304         try {
  305             if (loader[*i](*this, file)) {
  306                 if (options.verbose > 1)
  307                     cout << "todo: loaded database successfully with '" << (*i) << "' loader" << endl;
  308                 triggerEvent("load");
  309                 return;
  310             }
  311         } catch (std::exception &e) {
  312             lastError = e.what();
  313         }
  314     }
  315     throw exception("no database loaders for database format or database corrupt (last error was '" + lastError + "'");
  316 }
  317 
  318 void TodoDB::save(multiset<Todo> const &todo, ostream &of, int ind) {
  319 
  320     for (multiset<Todo>::const_iterator i = todo.begin(); i != todo.end(); i++)
  321     {
  322         cerr << "saving: " << i->text << endl;
  323         if (i->type == Todo::Link) {
  324             of  << string(ind * 4, ' ') << "<link"
  325                 << " filename=\"" << i->todofile << "\""
  326                 << " priority=\"" << symbolisePriority(i->priority) << "\""
  327                 << " time=\"" << i->added << "\""
  328                 << "/>" << endl;
  329 
  330             if (i->db)
  331                 i->db->save(i->todofile);
  332 
  333             /* Restore the TODODB environment variable. */
  334 string envar = "TODODB=" + filename;
  335 
  336             //setenv("TODODB", options.database.c_str(), 1);
  337             putenv(strdup(const_cast<char*>(envar.c_str())));
  338 
  339         }
  340         else {
  341             of  << string(ind * 4, ' ') << "<note"
  342                 << " priority=\"" << symbolisePriority((*i).priority) << "\""
  343                 << " time=\"" << (*i).added << "\"";
  344             if ((*i).done) {
  345                 of  << " done=\"" << (*i).doneTime<< "\"";
  346             }
  347             of << ">" << endl;
  348             of << string((ind + 1) * 4, ' ');
  349             of << htmlify((*i).text) << endl;
  350             if ((*i).comment != "") {
  351                 of << "<comment>" << endl;
  352                 of << string((ind + 2) * 4, ' ');
  353                 of << htmlify((*i).comment) << endl;
  354                 of << "</comment>" << endl;
  355             }
  356             save(*i->child, of, ind + 1);
  357             of << string(ind * 4, ' ');
  358             of << "</note>" << endl;
  359         }
  360     }
  361 }
  362 
  363 void TodoDB::save(string const &file) {
  364     // Do backups
  365     if (dirty && options.backups) {
  366     string newname;
  367         for (int i = options.backups - 1; i > 0; i--) {
  368             newname = file + "." + stringify(i + 1);
  369             if (options.verbose > 1)
  370                 cout << "todo: renaming " << file << "." << i  << " to " << file << "." << i + 1 << endl;
  371             chmod(newname.c_str(), 0600);
  372             ::unlink(newname.c_str());
  373             rename((file + "." + stringify(i)).c_str(), newname.c_str());
  374             chmod(newname.c_str(), 0400);
  375         }
  376         if (options.verbose > 1)
  377             cout << "todo: renaming " << file << " to " << file << ".1" << endl;
  378         newname = file + ".1";
  379         chmod(newname.c_str(), 0600);
  380         ::unlink(newname.c_str());
  381         rename(file.c_str(), newname.c_str());
  382         chmod(newname.c_str(), 0400);
  383     }
  384     if (todo.size() || titleText != "") {
  385     Saver saver = getSavers();
  386     string lastError;
  387 
  388         if (dirty && options.verbose > 1)
  389             cout << "todo: saving to database '" << file << "'" << endl;
  390 
  391         for (vector<string>::iterator i = options.loaders.begin(); i != options.loaders.end(); ++i) {
  392             if (dirty && options.verbose > 1)
  393                 cout << "todo: trying '" << (*i) << "' saver" << endl;
  394             if (saver.find(*i) == saver.end())
  395                 throw exception("couldn't find saver for '" + *i + "'");
  396             try {
  397                 if (saver[*i](*this, file)) {
  398                     // Preserve ownership and mode
  399                     if (statSuccessful) {
  400                         if (options.verbose > 1)
  401                             cout << "todo: preserving attributes" << endl;
  402                         chmod(file.c_str(), _stat.st_mode);
  403                         chown(file.c_str(), _stat.st_uid, _stat.st_gid);
  404                     } else {
  405                         triggerEvent("create");
  406                         if (options.paranoid) {
  407                             if (options.verbose > 1)
  408                                 cout << "todo: paranoia check" << endl;
  409                             stat(file.c_str(), &_stat);
  410                             if (_stat.st_mode & 0077)
  411                                 cerr << "todo: warning, created database (" << file << ") has group or world permissions" << endl;
  412                         }
  413                     }
  414                     triggerEvent("save");
  415                     return;
  416                 }
  417                 else if (!dirty)
  418                     return;
  419             } catch (std::exception &e) {
  420                 lastError = e.what();
  421             }
  422         }
  423         throw exception(lastError);
  424     throw exception(lastError);
  425     } else {
  426         if (options.verbose > 1)
  427             cout << "todo: empty database '" << file << "', unlinking" << endl;
  428         ::unlink(file.c_str());
  429     }
  430 }
  431 
  432 vector<string> TodoDB::getIndexList(string const &str) {
  433 vector<string> tmp = split(",", str), out;
  434 
  435     for (vector<string>::iterator i = tmp.begin(); i != tmp.end(); i++) {
  436         // wildcard?
  437         if ((*i)[(*i).size() - 1] == '*') {
  438         string base = (*i).substr(0, (*i).rfind("."));
  439         Todo *t = find(todo, base);
  440 
  441 
  442             if (!t) throw exception("can't expand non-existant wildcard note '" + (*i) + "'");
  443             for (multiset<Todo>::iterator j = t->child->begin(); j != t->child->end(); j++)
  444                 out.push_back(base + stringify((*j).index));
  445         } else
  446         // is it a range?
  447         if ((*i).find('-') != string::npos) {
  448         string start = (*i).substr(0, (*i).find('-')), 
  449             end = (*i).substr(start.size() + 1);
  450 
  451             if (end.find('.') != string::npos)
  452                 throw exception("ranges are in the form 'x.y.z1-z2' not '" + (*i) + "'");
  453 
  454         int a = destringify<int>(start.substr(start.rfind('.') + 1)),
  455             b = destringify<int>(end);
  456 
  457             if (b < a) {
  458             int swap = a;
  459 
  460                 a = b;
  461                 b = swap;
  462             }
  463 
  464         string base = start.substr(0, (*i).rfind('.') + 1);
  465 
  466             for (int i = a; i <= b; ++i)
  467                 out.push_back(base + stringify(i));
  468         } else
  469             out.push_back((*i));
  470     }
  471     return out;
  472 }
  473 
  474 /*  Get a priority level from the user. If the user has passed a priority
  475     level on the command line it will be used instead of interactively
  476     requesting a priority level.
  477 
  478     This function allows for tab completion of priority levels as well
  479     as the levels being in the command history.
  480 */
  481 Todo::Priority TodoDB::getPriority(string current)
  482 {
  483 const char *pri[] = {
  484     "veryhigh",
  485     "high",
  486     "medium",
  487     "low",
  488     "verylow",
  489 };
  490 
  491 string priority;
  492 
  493     if (isatty(0)) {
  494         // Display list of priorities if at a tty
  495         for (int i = 0; i < 5; i++) {
  496             cout << info << i + 1 << ". " << TodoDB::priority[i] << pri[i] << normal << "   ";
  497             addHistory(pri[i]);
  498         }
  499         cout << endl;
  500     }
  501     while (true) {
  502         if (isatty(0)) {
  503             if (options.verbose)
  504                 cout << info << "Enter a priority from those listed above." << normal << endl;
  505             priority = readText("priority> ", current, true);
  506         } else
  507             priority = current;
  508         priority = trim(priority);
  509         // Default to medium
  510         if (priority == "") priority = "medium";
  511         try {
  512             if (priority.size() == 1 && isdigit(priority[0])) {
  513             int index = destringify<int>(priority);
  514 
  515                 if (index < 1 || index > 5) throw "invalid priority";
  516                 return desymbolisePriority(pri[index - 1]);
  517             }
  518             return desymbolisePriority(symbolisePriority(priority));
  519         } catch (...) {
  520             cout << "error: invalid priority" << endl;
  521         }
  522     }
  523 }
  524 
  525 void TodoDB::add() {
  526 string text;
  527 Todo *parent = 0;
  528 
  529     // grafting, get parents priority and use it as default for children
  530     if (options.index.size()) {
  531         parent = find(todo, options.index[0]);
  532 
  533         if (!parent) throw exception("couldn't find '" + options.index[0] + "' to graft to"); 
  534     }
  535 
  536     if (options.text != "")
  537         text = options.text;
  538     else
  539     {
  540         if (isatty(0)) {
  541             if (options.verbose)
  542                 cout << info << "Enter text for the item you are adding." << normal << endl;
  543             text = readText("text> ", text);
  544         } else {
  545         string line;
  546 
  547             while (getline(cin, line))
  548                 text += line + '\n';
  549         }
  550     }
  551 
  552 Todo t;
  553 
  554     t.text = text;
  555 
  556     if (options.priority == Todo::Default)
  557         if (parent)
  558             options.priority = parent->priority;
  559         else
  560             options.priority = Todo::Medium;
  561 
  562     if (options.priority != Todo::None)
  563         t.priority = options.priority;
  564     else {
  565     string defpri;
  566 
  567         if (parent)
  568             defpri = symbolisePriority(parent->priority);
  569         else
  570             defpri = "medium";
  571         t.priority = getPriority(defpri);
  572     }
  573     t.added = getCurrentDate();
  574     // grafting
  575     if (parent) {
  576         if (options.verbose > 1)
  577             cout << "todo: grafting new item to item " << options.index[0] << endl;
  578         parent->child->insert(t);
  579         parent->db->setDirty(true);
  580 
  581         if (options.verbose) {
  582         unsigned findindex = 1;
  583 
  584             for (multiset<Todo>::iterator i = parent->child->begin(); i != parent->child->end(); ++i, findindex++)
  585                 if (t == *i)
  586                     cout << info << "Index of new item is " << options.index[0] << "." << findindex << normal << endl;
  587         }
  588     } else {
  589         if (options.verbose > 1)
  590             cout << "todo: adding new item" << endl;
  591         todo.insert(t);
  592         setDirty(true);
  593 
  594         if (options.verbose) {
  595         unsigned findindex = 1;
  596             for (multiset<Todo>::iterator i = todo.begin(); i != todo.end(); ++i, findindex++)
  597                 if (t == *i)
  598                     cout << info << "Index of new item is " << findindex << normal << endl;
  599         }
  600     }
  601 
  602 }
  603 
  604 void TodoDB::link() {
  605 string filename;
  606 Todo *parent = 0;
  607 
  608     // grafting, get parents priority and use it as default for children
  609     if (options.index.size()) {
  610         parent = find(todo, options.index[0]);
  611 
  612         if (!parent) throw exception("couldn't find '" + options.index[0] + "' to graft to");
  613     }
  614 
  615     if (options.filename != "")
  616         filename = options.filename;
  617     else
  618     {
  619         if (isatty(0)) {
  620             if (options.verbose)
  621                 cout << info << "Enter the filename of the todo database to link." << normal << endl;
  622 
  623             filename = readText("filename> ", filename);
  624         } else
  625         {
  626         string line;
  627 
  628             // XXX
  629             while (getline(cin, line))
  630                 filename += line + '\n';
  631         }
  632     }
  633 
  634 Todo t;
  635 
  636     t.type = Todo::Link;
  637 
  638     if (parent)
  639         t.todofile = fixRelativePath(parent->db->basepath, filename);
  640     else
  641         t.todofile = filename;
  642 
  643 ifstream in(t.todofile.c_str());
  644     if (!in) throw exception("could not link to '" + t.todofile + "', unreadable/not found");
  645 
  646     if (options.priority == Todo::Default)
  647         options.priority = Todo::Medium;
  648 
  649     if (options.priority != Todo::None)
  650         t.priority = options.priority;
  651     else {
  652         string defpri;
  653 
  654         if (parent)
  655             defpri = symbolisePriority(parent->priority);
  656         else
  657             defpri = "medium";
  658 
  659         t.priority = getPriority(defpri);
  660     }
  661 
  662     t.added = getCurrentDate();
  663 
  664     // grafting
  665     if (parent) {
  666         if (options.verbose > 1)
  667             cout << "todo: grafting new item to item " << options.index[0] << endl;
  668 
  669         parent->child->insert(t);
  670         parent->db->setDirty(true);
  671 
  672         if (options.verbose) {
  673             unsigned findindex = 1;
  674 
  675                 for (multiset<Todo>::iterator i = parent->child->begin(); i != parent->child->end(); ++i, findindex++)
  676                     if (t == *i)
  677                         cout << info << "Index of new item is " << options.index[0] << "." << findindex << normal << endl;
  678         }
  679     } else {
  680         if (options.verbose > 1)
  681             cout << "todo: adding new item" << endl;
  682 
  683         t.todofile = filename;
  684 
  685         todo.insert(t);
  686 
  687         setDirty(true);
  688 
  689         if (options.verbose) {
  690         unsigned findindex = 1;
  691 
  692             for (multiset<Todo>::iterator i = todo.begin(); i != todo.end(); ++i, findindex++)
  693                 if (t == *i)
  694                     cout << info << "Index of new item is " << findindex << normal << endl;
  695         }
  696     }
  697 }
  698 
  699 void TodoDB::edit() {
  700     if (options.index.size() == 0) throw exception("no notes specified to edit");
  701 string const &index = options.index[0];
  702 Todo t = *find(todo, index);
  703 
  704     if (t.type == Todo::Link)
  705         throw exception("can't edit the body of a link");
  706     erase(todo, options.index[0]);
  707     if (t.done) throw exception("you can't edit an item that is done");
  708     if (options.verbose)
  709         cout << info << "Modify the text of the item you are editing." << normal << endl;
  710     t.text = readText("text> ", t.text);
  711 
  712     if (options.priority != Todo::None && options.priority != Todo::Default)
  713         t.priority = options.priority;
  714 
  715     if (options.priority != Todo::Default)
  716         t.priority = getPriority(symbolisePriority(t.priority));
  717 
  718     if (options.comment || t.comment != "") {
  719         if (options.verbose)
  720             cout << info << "Enter comment for this item." << normal << endl;
  721         t.comment = readText("comment> ", t.comment);
  722     }
  723 
  724     if (index.find(".") != string::npos) {
  725 string parent = index.substr(0, index.rfind("."));
  726 
  727     Todo *p = find(todo, parent);
  728 
  729         p->child->insert(t);
  730         p->db->setDirty(true);
  731     } else {
  732         t.db = this;
  733         todo.insert(t);
  734         setDirty(true);
  735     }
  736 }
  737 
  738 void TodoDB::remove() {
  739 vector<string> remove = options.index;
  740 vector<string> notfound;
  741 int erased = 0;
  742 
  743     for (vector<string>::iterator j = remove.begin(); j != remove.end(); j++) {
  744     Todo *t = find(todo, *j);
  745 
  746         if (t) {
  747             if (t->type == Todo::Link && options.verbose)
  748                 cout << info << "todo: removing link to database '" << t->todofile << "'" << normal << endl;
  749             erased++;
  750             if (options.verbose > 1)
  751                 cout << info << "todo: permanently removing item '" << (*j) << "'" << normal << endl;
  752             t->db->setDirty(true);
  753             erase(todo, *j);
  754         } else
  755             notfound.push_back(*j);
  756     }
  757     if (notfound.size())
  758         throw exception("couldn't erase records '" + join(",", notfound) + "'");
  759     if (options.verbose)
  760         cout << info << "todo: erased " << erased << " records" << normal << endl;
  761 }
  762 
  763 void TodoDB::filterChildren(Todo &todo, FilterChildren filter) {
  764     todo.filtered = filter == FILTERED ? true : false;
  765     if (!todo.filtered && todo.parent) 
  766         todo.incUnFilteredChildren();
  767     if (filter == NOTFILTERED)
  768         for (multiset<Todo>::iterator i = todo.child->begin(); i != todo.child->end(); i++)
  769             filterChildren((Todo&)*i, NOTFILTERED);
  770     else
  771     if (filter == CHILDRENNOTFILTERED)
  772         for (multiset<Todo>::iterator i = todo.child->begin(); i != todo.child->end(); i++) {
  773             if ((
  774                 // ugh
  775                 options.filter.done && (
  776                     (options.filter.donedir == Options::Positive) ||
  777                     (options.filter.donedir == Options::Equal && i->done) ||
  778                     (options.filter.donedir == Options::Negative && !i->done)
  779                     )
  780                 ) || 
  781                 (!options.filter.done && !i->done)
  782                 ) {
  783                 // doesn't change sort order
  784                 const_cast<Todo*>(&*i)->filtered = false;
  785                 const_cast<Todo*>(&*i)->incUnFilteredChildren();
  786             }
  787         }
  788 }
  789 
  790 void TodoDB::filterView(multiset<Todo> &todo) {
  791 int n = 1;
  792 
  793     for (multiset<Todo>::iterator i = todo.begin(); i != todo.end(); i++, n++) {
  794     Todo &todo = const_cast<Todo&>(*i);
  795     Options::Filter &filter = options.filter;
  796     bool skipchildren = false;
  797 
  798         if (filter.show == Options::Negative) 
  799             skipchildren = true;
  800         else
  801         if (filter.show == Options::Positive)
  802             filterChildren(todo, NOTFILTERED);
  803         else {
  804             // Filter on done
  805             if (filter.done) {
  806                 // Don't show done notes
  807                 if (filter.donedir == Options::Negative) {
  808                     if (!todo.done) todo.filtered = false;
  809                 } else
  810                 // Only show done notes
  811                 if (filter.donedir == Options::Equal) {
  812                     if (todo.done) todo.filtered = false;
  813                 } else
  814                     todo.filtered = false;
  815             }
  816 
  817             // Filter on priority
  818             if (!todo.filtered && filter.priority != Todo::None) {
  819                 todo.filtered = true;
  820                 if (filter.prioritydir == Options::Negative) {
  821                     if (todo.priority <= filter.priority) 
  822                         todo.filtered = false;
  823                 } else
  824                 if (filter.prioritydir == Options::Positive) {
  825                     if (todo.priority >= filter.priority) 
  826                         todo.filtered = false;
  827                 } else
  828                     if (todo.priority == filter.priority) 
  829                         todo.filtered = false;
  830             }
  831 
  832             if (filter.children) {
  833                 // Don't show children
  834                 if (filter.childrendir == Options::Negative) {
  835                 bool state = todo.filtered;
  836 
  837                     filterChildren(todo, FILTERED);
  838                     todo.filtered = state;
  839                     skipchildren = true;
  840                 }
  841             }
  842 
  843             // Search filter - after all other filters
  844             if (!todo.filtered && filter.search.source().size() != 0 && filter.search.match(todo.text.c_str()) == -1)
  845                 todo.filtered = true;
  846         }
  847 
  848         if (!todo.filtered)
  849             todo.incUnFilteredChildren();
  850 
  851         //if (!todo.filterchildren)
  852         if (!skipchildren) filterView(*todo.child);
  853     }
  854 }
  855 
  856 void TodoDB::filterView() {
  857 
  858     fixParents(todo);
  859 
  860     filterView(todo);
  861 
  862     // filter items based on explicit numeric
  863     for (map<string,Options::Dir>::const_iterator i = options.filter.item.begin(); i != options.filter.item.end(); ++i) {
  864     Todo *t = find(todo, (*i).first);
  865 
  866         if ((*i).second == Options::Negative)
  867             filterChildren(*t, FILTERED);
  868         else
  869         if ((*i).second == Options::Positive)
  870             filterChildren(*t, NOTFILTERED);
  871         else
  872             filterChildren(*t, CHILDRENNOTFILTERED);
  873     }
  874 }
  875 
  876 void TodoDB::view(multiset<Todo> const &todo, int ind) {
  877 
  878     for (multiset<Todo>::iterator i = todo.begin(); i != todo.end(); i++) {
  879     Todo const &todo = (*i);
  880 
  881         if (!todo.filtered) {
  882             if (options.verbose)
  883                 formatItem(cout, ind, todo, options.format["verbose-display"]);
  884             else
  885                 formatItem(cout, ind, todo, options.format["display"]);
  886             if (options.comment && todo.comment != "") {
  887             int indent = 4 * (ind + 1);
  888 
  889                 cout << comment << string(indent, ' ');
  890                 if (options.summary) {
  891                 const string s = todo.comment;
  892 
  893                     if (s.find('\n') != string::npos) {
  894                         if ((int)s.find('\n') < options.columns - 1 - indent)
  895                             cout << s.substr(0, s.find('\n')) << info << "+" << normal;
  896                         else
  897                             cout << s.substr(0, options.columns - 1 - indent) << info << "+" << normal;
  898                     } else
  899                     if ((int)s.size() > options.columns - 3 - indent)
  900                         cout << "(" << s.substr(0, options.columns - 3 - indent) << ")" << info << "+";
  901                     else
  902                         cout << "(" << s << ")";
  903                 } else
  904                     wraptext(cout, "(" + todo.comment + ")", indent, indent, options.columns);
  905                 cout << normal << endl;
  906             }
  907         }
  908 
  909         //if (todo.filterchildren && !todo.unfilteredchildren) continue;
  910         if (todo.filtered && todo.unfilteredchildren)
  911             cout << string(4 * ind, ' ') << info << todo.index << "..." << normal << endl;
  912         view(*todo.child, ind + 1);
  913     }
  914 }
  915 
  916 void TodoDB::view() {
  917     if (titleText != "") {
  918         cout << title;
  919         wraptext(cout, titleText, 4, 0, options.columns - 8);
  920         cout << endl << normal;
  921     }
  922     filterView();
  923     if (options.verbose > 1)
  924         cout << "todo: displaying using format '" << options.format["verbose-display"] << "'" << endl;
  925     view(todo, 0);
  926 }
  927 
  928 void TodoDB::generate(ostream &out, multiset<Todo> const &todo, int ind) {
  929 
  930     for (multiset<Todo>::iterator i = todo.begin(); i != todo.end(); i++) {
  931     Todo const &todo = (*i);
  932 
  933         if (!todo.filtered)
  934             if (options.verbose) {
  935                 formatItem(out, ind, todo, options.format["verbose-generated"]);
  936             } else
  937                 formatItem(out, ind, todo, options.format["generated"]);
  938         //if (todo.filterchildren) continue;
  939         if (todo.filtered && todo.unfilteredchildren)
  940             out << string(4 * ind, ' ') << info << todo.index << "..." << normal << endl;
  941         generate(out, *todo.child, ind + 1);
  942     }
  943 }
  944 
  945 void TodoDB::generate()
  946 {
  947 ofstream out("TODO");
  948 
  949     if (out.bad()) 
  950         throw exception("couldn't open TODO for generation");
  951 
  952     if (titleText != "") wraptext(out, titleText, 0, options.columns);
  953     if (options.verbose > 1)
  954         cout << "todo: generating using format '" << options.format["verbose-generated"] << "'" << endl;
  955     filterView();
  956     generate(out, todo, 0);
  957     if (options.verbose)
  958         cout << info << "todo: generated TODO from current database" << normal << endl;
  959 }
  960 
  961 void TodoDB::purge() {
  962 time_t now = getCurrentDate();
  963 
  964 unsigned purged = purge(todo, now - options.purgeage * 86400);
  965     if (options.verbose)
  966         cout << info << "todo: purged " << purged << " completed items older than " << options.purgeage << " days" << normal << endl;
  967 }
  968 
  969 unsigned TodoDB::purge(multiset<Todo> &todo, time_t age) {
  970 unsigned purged = 0;
  971 
  972     for (multiset<Todo>::iterator i = todo.begin(); i != todo.end(); ++i) {
  973         if (i->done && i->doneTime < age) {
  974         multiset<Todo>::iterator last = i++;
  975 
  976             cout << i->doneTime << " < " << age << endl;
  977             last->db->setDirty(true);
  978             todo.erase(last);
  979             ++purged;
  980         } else {
  981             if (i->child) {
  982                 purged += purge(*i->child, age);
  983             }
  984         }
  985     }
  986     return purged;
  987 }
  988 
  989 void TodoDB::stats(multiset<Todo> const &todo, int ind) {
  990 }
  991 
  992 void TodoDB::stats() {
  993     filterView();
  994     if (options.verbose > 1)
  995         cout << "todo: displaying using format '" << options.format["verbose-display"] << "'" << endl;
  996     view(todo, 0);
  997 }
  998 
  999 int TodoDB::markDone(Todo &todo) {
 1000 int count = 1;
 1001 
 1002     todo.done = true;
 1003     for (multiset<Todo>::iterator i = todo.child->begin(); i != todo.child->end(); i++)
 1004         count += markDone(const_cast<Todo&>(*i));
 1005     return count;
 1006 }
 1007 
 1008 void TodoDB::done() {
 1009 vector<string> done = options.index, notfound;
 1010 int marked = 0;
 1011 
 1012     for (vector<string>::iterator j = done.begin(); j != done.end(); j++) {
 1013     Todo *t = find(todo, *j);
 1014 
 1015         if (t) {
 1016             marked += markDone(*t);
 1017             if (options.verbose > 1)
 1018                 cout << "todo: marked '" << *j << "' as done" << endl;
 1019             t->doneTime = getCurrentDate();
 1020             if (options.verbose)
 1021                 cout << info << "Enter comment for this item." << normal << endl;
 1022             t->comment = readText("comment> ", t->comment);
 1023             t->db->setDirty(true);
 1024         } else
 1025             notfound.push_back(*j);
 1026     }
 1027     if (notfound.size())
 1028         throw exception("couldn't mark records as done '" + join(",", notfound) + "'");
 1029     if (options.verbose)
 1030         cout << info << "todo: marked " << marked << " records as done" << normal << endl;
 1031 }
 1032 
 1033 int TodoDB::markNotDone(Todo &todo) {
 1034 int count = 1;
 1035 
 1036     todo.done = false;
 1037     for (multiset<Todo>::iterator i = todo.child->begin(); i != todo.child->end(); i++)
 1038         count += markNotDone(const_cast<Todo&>(*i));
 1039     return count;
 1040 }
 1041 
 1042 void TodoDB::notdone() {
 1043 vector<string> done = options.index, notfound;
 1044 int marked = 0;
 1045 
 1046     for (vector<string>::iterator j = done.begin(); j != done.end(); j++) {
 1047     Todo *t = find(todo, *j);
 1048 
 1049         if (t) {
 1050             marked += markNotDone(*t);
 1051             if (options.verbose > 1)
 1052                 cout << "todo: marked '" << *j << "' as not done" << endl;
 1053             t->db->setDirty(true);
 1054         } else
 1055             notfound.push_back(*j);
 1056     }
 1057     if (notfound.size())
 1058         throw exception("couldn't mark records as not done '" + join(",", notfound) + "'");
 1059     if (options.verbose)
 1060         cout << info << "todo: marked " << marked << " records as not done" << normal << endl;
 1061 }
 1062 
 1063 void TodoDB::setColour(string const &item, string const &colour) {
 1064     if (streamColour.find(item.c_str()) == streamColour.end()) throw exception("unknown item '" + item + "' can't be coloured");
 1065 
 1066 ostream &(*c)(ostream &);
 1067 ostream &(*a)(ostream &) = StreamColour::mono;
 1068 string clr = colour;
 1069 
 1070     if (clr[0] == '+') {
 1071         a = ::bold;
 1072         clr = colour.substr(1);
 1073     }
 1074     if (clr == "black") c = ::black;
 1075     else
 1076     if (clr == "red") c = ::red;
 1077     else
 1078     if (clr == "green") c = ::green;
 1079     else
 1080     if (clr == "yellow") c = ::yellow;
 1081     else
 1082     if (clr == "blue") c = ::blue;
 1083     else
 1084     if (clr == "magenta") c = ::magenta;
 1085     else
 1086     if (clr == "cyan") c = ::cyan;
 1087     else
 1088     if (clr == "white") c = ::white;
 1089     else
 1090     if (clr == "default") c = StreamColour::mono;
 1091     else
 1092         throw exception("unknown colour '" + clr + "'");
 1093     streamColour[item].attribute = a;
 1094     streamColour[item].colour = c;
 1095 }
 1096 
 1097 void TodoDB::reparent() {
 1098 string aps = options.index[0];
 1099 Todo *a = find(aps);
 1100 multiset<Todo> *ap = 0;
 1101 
 1102 int delim = aps.rfind('.');
 1103     if (delim != -1) {
 1104         aps = aps.substr(0, delim);
 1105         ap = &(*find(aps)->child);
 1106     } else
 1107         ap = &todo;
 1108 
 1109 Todo tmp = *a;
 1110     for (multiset<Todo>::iterator i = ap->begin(); i != ap->end(); ++i)
 1111         if (&(*i) == a) {
 1112             i->db->setDirty(true);
 1113             ap->erase(i);
 1114             break;
 1115         }
 1116 
 1117     if (options.index.size() > 1) {
 1118         Todo *t = find(options.index[1]);
 1119         tmp.parent = t;
 1120         tmp.db = t->db;
 1121         t->child->insert(tmp);
 1122         t->db->setDirty(true);
 1123     } else {
 1124         tmp.db = this;
 1125         todo.insert(tmp);
 1126         setDirty(true);
 1127     }
 1128     
 1129 
 1130 //  tmp.child = NULL;
 1131 }
 1132 
 1133 ostream &TodoDB::StreamColour::veryhigh(ostream &os) { return os << normal << streamColour["veryhigh"].attribute << streamColour["veryhigh"].colour; }
 1134 ostream &TodoDB::StreamColour::high(ostream &os) { return os << normal << streamColour["high"].attribute << streamColour["high"].colour; }
 1135 ostream &TodoDB::StreamColour::medium(ostream &os) { return os << normal << streamColour["medium"].attribute << streamColour["medium"].colour; }
 1136 ostream &TodoDB::StreamColour::low(ostream &os) { return os << normal << streamColour["low"].attribute << streamColour["low"].colour; }
 1137 ostream &TodoDB::StreamColour::verylow(ostream &os) { return os << normal << streamColour["verylow"].attribute << streamColour["verylow"].colour; }
 1138 ostream &TodoDB::StreamColour::info(ostream &os) { return os << normal << streamColour["info"].attribute << streamColour["info"].colour; }
 1139 ostream &TodoDB::StreamColour::title(ostream &os) { return os << normal << streamColour["title"].attribute << streamColour["title"].colour; }
 1140 ostream &TodoDB::StreamColour::comment(ostream &os) { return os << normal << streamColour["comment"].attribute << streamColour["comment"].colour; }
 1141 ostream &TodoDB::StreamColour::mono(ostream &os) { return os; }
 1142 ostream &TodoDB::StreamColour::normal(ostream &os) { return os << ::normal; }
 1143 
 1144 /*
 1145     Template string for formatting output. For example, 
 1146         %i%[info]%f%2n.%[priority]%T\n
 1147     would generate the default display.
 1148 
 1149     - %<n>i: indent to current depth; <n> is the number of spaces per 
 1150       indent level and defaults to 4
 1151     - %T is the item text, which wraps and indents to the depth the item
 1152       started at
 1153     - %t is unwrapped text
 1154     - %p is the priority
 1155     - %c is the creation date (formatted according to --format-date)
 1156     - %d is the completion (done) date
 1157     - %D is the time it took to complete the item
 1158     - %<n>n is the index number of the item; <n> is the amount of characters the
 1159       number should take up - padded with spaces
 1160     - %f is the state flag (+ means children, - means done, * means 
 1161       children and done)
 1162     - %[<colour>] to specify a colour (an additional colour is 'priority' 
 1163       which defaults to the current items priority colour). 
 1164       eg. %[priority]
 1165     - %s is a summary (one line) of the text body
 1166 
 1167 */
 1168 void TodoDB::formatItem(ostream &out, int depth, Todo const &item, string const &format) {
 1169 int indent = 0;
 1170 int defaultindent = 4;
 1171 Options::Dir dir = Options::Equal;
 1172 
 1173     for (unsigned i = 0; i < format.size(); ++i) {
 1174     int multiplier = -1;
 1175 
 1176         switch (format[i]) {
 1177             case '%' :
 1178                 ++i;
 1179                 if (isdigit(format[i]) || format[i] == '-' || format[i] == '+') {
 1180                     if (format[i] == '-') {
 1181                         dir = Options::Negative;
 1182                         ++i;
 1183                     } else
 1184                     if (format[i] == '+') {
 1185                         dir = Options::Positive;
 1186                         ++i;
 1187                     } else
 1188                         dir = Options::Equal;
 1189                     multiplier = destringify<int>(format.c_str() + i);
 1190                     while (format[i] && (isdigit(format[i]) || 
 1191                         format[i] == '-' || format[i] == '+')) ++i;
 1192                 }
 1193                 switch (format[i]) {
 1194                     case '%' : out << '%'; break;
 1195                     case '>' :
 1196                         if (multiplier == -1)
 1197                             throw exception("'>' formatting flag requires numeric prefix");
 1198                         defaultindent = multiplier;
 1199                     break;
 1200                     // indent
 1201                     case 'i' : {
 1202                     int i = 0;
 1203 
 1204                         i = depth;
 1205                         if (dir != 0)
 1206                             i += dir * multiplier;
 1207                         i *= defaultindent;
 1208                         for (; i > indent; indent++)
 1209                             out << ' ';
 1210                     }
 1211                     break;
 1212                     case 'T' : {
 1213                     int i = 0;
 1214 
 1215                         i = depth;
 1216                         if (dir != 0)
 1217                             i += dir * multiplier;
 1218                         i *= defaultindent;
 1219                         if (options.summary) {
 1220                         const string s = item.text;
 1221 
 1222                             if (s.find('\n') != string::npos) {
 1223                                 if ((int)s.find('\n') < options.columns - 1 - indent)
 1224                                     out << s.substr(0, s.find('\n')) << info << "+" << normal;
 1225                                 else
 1226                                     out << s.substr(0, options.columns - 1 - indent) << info << "+" << normal;
 1227                             } else
 1228                             if ((int)s.size() > options.columns - 1 - indent)
 1229                                 out << s.substr(0, options.columns - 1 - indent) << info << "+" << normal;
 1230                             else
 1231                                 out << s;
 1232                         } else
 1233                             wraptext(out, item.text, i, indent, options.columns);
 1234                     }
 1235                     break;
 1236                     case 't' :
 1237                         if (options.summary) {
 1238 /*                          if ((int)item.text.size() > options.columns - 1 - indent)
 1239                                 out << item.text.substr(0, options.columns - 1 - indent) << info << "+" << normal << endl;
 1240                             else
 1241                                 out << item.text << endl;
 1242                             indent = 0;*/
 1243                         const string s = item.text;
 1244 
 1245                             if (s.find('\n') != string::npos) {
 1246                                 if ((int)s.find('\n') < options.columns - 1 - indent)
 1247                                     out << s.substr(0, s.find('\n')) << info << "+" << normal << endl;
 1248                                 else
 1249                                     out << s.substr(0, options.columns - 1 - indent) << info << "+" << normal << endl;
 1250                             } else
 1251                             if ((int)s.size() > options.columns - 1 - indent)
 1252                                 out << s.substr(0, options.columns - 1 - indent) << info << "+" << normal << endl;
 1253                             else
 1254                                 out << s << endl;
 1255                             indent = 0;
 1256                         } else {
 1257                             out << item.text;
 1258                             indent += item.text.size();
 1259                         }
 1260                     break;
 1261                     case 's' :
 1262                         if ((int)item.text.size() > options.columns - 1 - indent)
 1263                             out << item.text.substr(0, options.columns - 1 - indent) << info << "+" << normal << endl;
 1264                         else
 1265                             out << item.text << endl;
 1266                         indent = 0;
 1267                     break;
 1268                     case 'p' : {
 1269                     string s = symbolisePriority(item.priority);
 1270                         out << s;
 1271                         indent += s.size();
 1272                     }
 1273                     break;
 1274                     case 'c' : {
 1275                     string s = dateToHuman(item.added);
 1276                         out << s;
 1277                         indent += s.size();
 1278                     }
 1279                     break;
 1280                     case 'd' : {
 1281                     string s;
 1282                     
 1283                         if (item.done)
 1284                             s = "completed on " + dateToHuman(item.doneTime);
 1285                         else
 1286                             s = "incomplete";
 1287                         out << s;
 1288                         indent += s.size();
 1289                     }
 1290                     break;
 1291                     case 'D' :
 1292                         if (item.done) {
 1293                         string s = elapsedToHuman(item.added, item.doneTime) + " elapsed";
 1294                             out << s;
 1295                             indent += s.size();
 1296                         } else {
 1297                             out << "incomplete";
 1298                             indent += 12;
 1299                         }
 1300                     break;
 1301                     case 'n' : {
 1302                     string s = stringify(item.index);
 1303                         if (multiplier != -1 && (int)s.size() < multiplier) {
 1304                             out << string(multiplier - s.size(), ' ');
 1305                             indent += multiplier - s.size();
 1306                         }
 1307                         out << s;
 1308                         indent += s.size();
 1309                     }
 1310                     break;
 1311                     case 'F' : {
 1312                     string s;
 1313                     bool filteredchildren = false;
 1314 
 1315                         for (multiset<Todo>::const_iterator i = item.child->begin(); i != item.child->end(); i++)
 1316                             if ((*i).filtered) {
 1317                                 filteredchildren = true;
 1318                                 break;
 1319                             }
 1320 
 1321                         if (item.done) {
 1322                             if (filteredchildren)
 1323                                 s = "done, children";
 1324                             else
 1325                                 s = "done";
 1326                         } else
 1327                         if (filteredchildren)
 1328                             s = "children";
 1329                         else
 1330                             s = "open";
 1331                         out << s;
 1332                         indent += s.size();
 1333                     }
 1334                     break;
 1335                     case 'f' : {
 1336                     bool filteredchildren = false;
 1337 
 1338                         for (multiset<Todo>::const_iterator i = item.child->begin(); i != item.child->end(); i++)
 1339                             if ((*i).filtered) {
 1340                                 filteredchildren = true;
 1341                                 break;
 1342                             }
 1343 
 1344                         if (item.done) {
 1345                             if (filteredchildren)
 1346                                 out << '*';
 1347                             else
 1348                                 out << "-";
 1349                         } else
 1350                         if (filteredchildren)
 1351                             out << "+";
 1352                         else
 1353                             out << " ";
 1354                         indent++;
 1355                     } break;
 1356                     case '[' : {
 1357                         i++;
 1358                         if (format.find(']', i) == string::npos)
 1359                             throw exception("no matching ']' found for format string");
 1360                     string colour = format.substr(i, format.find(']', i) - i);
 1361                         i = format.find(']', i);
 1362                         if (colour == "priority")
 1363                             out << priority[4 - item.priority];
 1364                         else
 1365                         if (colour == "info")
 1366                             out << info;
 1367                         else
 1368                         if (colour == "normal")
 1369                             out << normal;
 1370                         else
 1371                         if (colour == "comment")
 1372                             out << comment;
 1373                         else
 1374                             out << priority[4 - desymbolisePriority(colour)];
 1375                     }
 1376                     break;
 1377                     default :
 1378                         throw exception(stringify("unhandled formatting flag '") + format[i] + "'");
 1379                     break;
 1380                 }
 1381             break;
 1382             // Handle some C-style escape characters
 1383             case '\\' :
 1384                 ++i;
 1385                 switch (format[i]) {
 1386                     case 'n' : out << '\n'; indent = 0; break;
 1387                     case 't' :
 1388                         out << ' ';
 1389                         for (indent++; indent % 8 != 0; indent++) out << ' ';
 1390                     break;
 1391                 }
 1392             break;
 1393             default :
 1394                 out << format[i];
 1395                 if (format[i] == '\n') indent = 0; else indent++;
 1396             break;
 1397         }
 1398     }
 1399     out << normal;
 1400 }
 1401 
 1402 void TodoDB::triggerEvent(string const &event) {
 1403     if (options.event.find(event) == options.event.end()) return;
 1404 vector<string> &v = options.event[event];
 1405 char const *argv[v.size() + 1];
 1406 
 1407     if (options.verbose > 1)
 1408         cout << "todo: event '" << event << "' triggered ('" << str::join(" ", options.event[event]) << "')" << endl;
 1409     argv[0] = "todo";
 1410     for (unsigned i = 0; i < v.size(); i++) {
 1411         string s = expandEnvars(v[i]);
 1412         argv[i + 1] = strdup(s.c_str());
 1413     }
 1414     parseArgs(*this, v.size() + 1, argv);
 1415 }
 1416 
 1417 string TodoDB::fixPath(string path)
 1418 {
 1419     string newPath;
 1420     string::iterator iter;
 1421     
 1422     for (iter = path.begin(); iter != path.end(); iter++)
 1423     {
 1424         if (*iter == '.')
 1425         {
 1426             if (*(iter + 1) == '.' && *(iter + 2) == '/')
 1427             {
 1428                 if (!newPath.empty())
 1429                 {
 1430                     newPath.erase(newPath.rfind("/", newPath.size() - 2) + 1);
 1431                 }
 1432 
 1433                 iter += 2;
 1434 
 1435                 continue;
 1436             }
 1437             else if (*(iter + 1) == '/')
 1438             {
 1439                 iter++;
 1440 
 1441                 continue;
 1442             }
 1443             else if (iter + 1 == path.end())
 1444                 continue;
 1445         }
 1446         else if (*iter == '/')
 1447         {
 1448             while (*(iter + 1) == '/')
 1449                 iter++;
 1450         }
 1451 
 1452         newPath.push_back(*iter);
 1453     }
 1454 
 1455     return newPath;
 1456 }
 1457 
 1458 string TodoDB::fixRelativePath(string base, string path)
 1459 {
 1460     char cwd[BUFSIZ];
 1461     string relBase, relDestPath;
 1462     string prefix;
 1463     string result;
 1464     size_t s;
 1465 
 1466     if (path[0] == '/')
 1467         return path;
 1468 
 1469     /* Chop off any trailing '/''s */
 1470     if ((s = base.find_last_not_of('/')) != string::npos)
 1471         base.erase(s + 1);
 1472 
 1473     if ((s = path.find_last_not_of('/')) != string::npos)
 1474         path.erase(s + 1);
 1475 
 1476     /* Get the working directory. */
 1477     getcwd(cwd, BUFSIZ);
 1478 
 1479     /* Build the paths appropriately. */
 1480     if (base[0] != '/')
 1481     {
 1482         relBase = cwd;
 1483         relBase += "/" + base;
 1484     }
 1485     else
 1486         relBase = base;
 1487 
 1488     relDestPath = cwd;
 1489     relDestPath += "/" + path;
 1490 
 1491     /* Fix up the paths to process stuff like '..' */
 1492     relBase = fixPath(relBase);
 1493     relDestPath = fixPath(relDestPath);
 1494 
 1495     /* See how much of the prefix we can chop off. */
 1496     s = 0;
 1497 
 1498     do
 1499     {
 1500         size_t s1 = relBase.find("/", s);
 1501 
 1502         if (s1 == 0 && relBase[0] != '/')
 1503             break;
 1504 
 1505         if (relBase.substr(0, s1) == relDestPath.substr(0, s1))
 1506             s = s1 + 1;
 1507         else
 1508             prefix = relBase.substr(0, s);
 1509 
 1510     } while (prefix.empty());
 1511 
 1512     relBase.erase(0, prefix.size());
 1513     relDestPath.erase(0, prefix.size());
 1514     
 1515     if (relBase.empty())
 1516         result = relDestPath;
 1517     else
 1518     {
 1519         /* Look at the number of '/''s in relBase */
 1520         size_t count = 0;
 1521 
 1522         for (s = 0; (s = relBase.find("/", s + 1)) != string::npos; count++)
 1523             ;
 1524 
 1525         for (size_t i = 0; i <= count; i++)
 1526             result += "../";
 1527 
 1528         result += relDestPath;
 1529     }
 1530 
 1531     return result;
 1532 }