root/todo/tags/0.1.20/src/TodoDB.cc

Revision 462, 40.9 kB (checked in by athomas, 1 year ago)

Dev Todo: Fixed #32, #29. Thanks!

Line 
1 // We don't want this freed by the delete function.
2 #include <map>
3 #include "Strings.h"
4 #include "TodoDB.h"
5 #include "Loaders.h"
6 #include "support.h"
7 #include "config.h"
8
9 using namespace term;
10 using namespace str;
11
12 static bool coloursInitialised = false;
13
14 map<string, TodoDB::StreamColour> TodoDB::streamColour;
15
16 TodoDB::TodoDB() {
17         if (!coloursInitialised) {
18                 initColour();
19                 coloursInitialised = true;
20         }
21 }
22
23 TodoDB::TodoDB(string const &file) {
24         initColour();
25         load(file);
26 }
27
28 TodoDB::~TodoDB() {
29 }
30
31 void TodoDB::initColour() {
32         streamColour["veryhigh"] = StreamColour(red, bold);
33         streamColour["high"] = StreamColour(yellow, bold);
34         streamColour["medium"] = StreamColour(::normal, StreamColour::mono);
35         streamColour["low"] = StreamColour(cyan, StreamColour::mono);
36         streamColour["verylow"] = StreamColour(blue, bold);
37         streamColour["info"] = StreamColour(green, StreamColour::mono);
38         streamColour["title"] = StreamColour(green, bold);
39         streamColour["comment"] = StreamColour(white, bold);
40 }
41
42 void TodoDB::initColourPost() {
43         if (options.mono) {
44                 streamColour["veryhigh"] = StreamColour(StreamColour::mono, StreamColour::mono);
45                 streamColour["high"] = StreamColour(StreamColour::mono, StreamColour::mono);
46                 streamColour["medium"] = StreamColour(StreamColour::mono, StreamColour::mono);
47                 streamColour["low"] = StreamColour(StreamColour::mono, StreamColour::mono);
48                 streamColour["verylow"] = StreamColour(StreamColour::mono, StreamColour::mono);
49                 streamColour["info"] = StreamColour(StreamColour::mono, StreamColour::mono);
50                 streamColour["title"] = StreamColour(StreamColour::mono, StreamColour::mono);
51                 streamColour["comment"] = StreamColour(StreamColour::mono, StreamColour::mono);
52                 priority[0] = StreamColour::mono;
53                 priority[1] = StreamColour::mono;
54                 priority[2] = StreamColour::mono;
55                 priority[3] = StreamColour::mono;
56                 priority[4] = StreamColour::mono;
57                 info = StreamColour::mono;
58                 title = StreamColour::mono;
59                 normal = StreamColour::mono;
60                 comment = StreamColour::mono;
61         } else {
62                 priority[0] = StreamColour::veryhigh;
63                 priority[1] = StreamColour::high;
64                 priority[2] = StreamColour::medium;
65                 priority[3] = StreamColour::low;
66                 priority[4] = StreamColour::verylow;
67                 info = StreamColour::info;
68                 title = StreamColour::title;
69                 normal = StreamColour::normal;
70                 comment = StreamColour::comment;
71         }
72
73 }
74
75 void TodoDB::operator () (Mode mode) {
76         initColourPost();
77         switch (mode) {
78                 case Add : triggerEvent("add"); add(); break;
79                 case Link : triggerEvent("link"); link(); break;
80                 case Remove : triggerEvent("remove"); remove(); break;
81                 case View : triggerEvent("view"); view(); break;
82                 case Edit : triggerEvent("edit"); edit(); break;
83                 case Generate : triggerEvent("generate"); generate(); break;
84                 case Done : triggerEvent("done"); done(); break;
85                 case NotDone : triggerEvent("notdone"); notdone(); break;
86                 case Title : triggerEvent("title"); edittitle(); break;
87                 case Reparent : triggerEvent("reparent"); reparent(); break;
88                 case Purge : triggerEvent("purge"); purge(); break;
89                 default :
90                         throw exception("unknown action?");
91                 break;
92         }
93 }
94
95 /*      Find an item.
96
97         Items are specified by their number. Sub-items are specified by a .
98         followed by their number, and so on. The kleene start can be used to
99         match any item at a level although wildcard matching does not actually
100         occur here, but in the getIndexList method.
101 */
102 Todo *TodoDB::find(multiset<Todo> const &todo, string const &index) {
103 int looking = destringify<int>(index);
104
105         for (multiset<Todo>::const_iterator i = todo.begin(); i != todo.end(); i++)
106                 if ((*i).index == looking) {
107                         // Recurse into child
108                         if (index.find(".") != string::npos) {
109                                 try {
110                                 string ns = index.substr(index.find(".") + 1);
111
112                                         return find(*i->child, ns);
113                                 } catch (exception &e) {
114                                         throw exception("couldn't find index '" + index + "'");
115                                 }
116                         }
117                         return const_cast<Todo*>(&(*i));
118                 }
119         throw exception("couldn't find index '" + index + "'");
120 }
121
122 multiset<Todo> &TodoDB::findContainer(multiset<Todo> &todo, string const &index) {
123 int looking = destringify<int>(index);
124
125         for (multiset<Todo>::iterator i = todo.begin(); i != todo.end(); i++)
126                 if ((*i).index == looking) {
127                         // Recurse into child
128                         if (index.find(".") != string::npos) {
129                                 try {
130                                 string ns = index.substr(index.find(".") + 1);
131
132                                         return findContainer(const_cast<multiset<Todo>& >(*i->child), ns);
133                                 } catch (exception &e) {
134                                         throw exception("couldn't find index '" + index + "'");
135                                 }
136                         }
137                         return todo;
138                 }
139         throw exception("couldn't find index '" + index + "'");
140 }
141
142 //      Erase the item with the specified index.
143 void TodoDB::erase(multiset<Todo> &todo, string const &index) {
144 int looking;
145
146         looking = destringify<int>(index);
147         for (multiset<Todo>::iterator i = todo.begin(); i != todo.end(); i++)
148                 if ((*i).index == looking) {
149                         // Recurse into child
150                         if (index.find(".") != string::npos) {
151                                 try {
152                                 string ss = index.substr(index.find(".") + 1);
153                                         erase(*const_cast<Todo&>(*i).child, ss);
154                                 } catch (exception &e) {
155                                         throw exception("couldn't find index '" + index + "'");
156                                 }
157                         } else {
158                                 todo.erase(i);
159                                 setDirty(true);
160                         }
161                         return;
162                 }
163         throw exception("couldn't find index '" + index + "'");
164 }
165
166 void TodoDB::edittitle() {
167 string text;
168
169         if (options.text != "") text = options.text;
170         else {
171                 if (isatty(0)) {
172                         if (options.verbose)
173                                 cout << info << "Enter text for the title of this todo list." << normal << endl;
174                         text = readText("text> ", titleText);
175                 } else {
176                 string line;
177
178                         while (getline(cin, line))
179                                 text += line + '\n';
180                 }
181         }
182         titleText = text;
183
184         setDirty(true);
185 }
186
187 void TodoDB::parse(vector<XML*>::const_iterator begin,
188         vector<XML*>::const_iterator end, multiset<Todo> &out) {
189         for (vector<XML*>::const_iterator i = begin; i != end; i++) {
190         XML &x = *(*i);
191
192                 switch (x.type()) {
193                         case XML::Element : {
194                                 if (x.name() == "title") {
195                                         titleText = trim((*x.child().begin())->body());
196                                         break;
197                                 } else
198                                 if (x.name() == "note") {
199                                 Todo todo;
200                                 // const_cast so I can use attrib[...]
201                                 map<string, string> &attrib = *const_cast<map<string, string>* >(&x.attrib());
202
203                                         if (attrib.find("priority") == attrib.end() || attrib.find("time") == attrib.end())
204                                                 throw exception("require both 'priority' and 'time' attributes for 'note' element");
205
206                                         if (attrib.find("done") != attrib.end()) {
207                                                         todo.done = true;
208                                                         todo.doneTime = destringify<time_t>(attrib["done"]);
209                                                 } else
210                                                         todo.done = false;
211
212                                         todo.priority = desymbolisePriority(attrib["priority"]);
213                                         todo.text = trim((*x.child().begin())->body());
214                                         todo.added = destringify<time_t>(attrib["time"]);
215
216                                         parse(x.child().begin(), x.child().end(), *todo.child);
217
218                                         out.insert(todo);
219                                 } else if (x.name() == "link") {
220                                 TodoDB newDb;
221                                 Todo todo;
222                                 map<string, string> &attrib = *const_cast<map<string, string>* >(&x.attrib());
223
224                                         if (attrib.find("filename") == attrib.end())
225                                                 throw exception("require 'filename' attribute for 'link' element");
226
227                                         newDb.load(attrib["filename"]);
228                                        
229                                         if (newDb.titleText == "")
230                                                 todo.text = attrib["filename"];
231                                         else
232                                                 todo.text = newDb.titleText;
233
234                                         todo.child = &newDb.todo;
235
236                                         out.insert(todo);
237                                 } else
238                                         throw exception("expected 'note' element, got '" + x.name() + "'");
239                         }
240                         break;
241                         default :
242                         break;
243                 }
244         }
245
246         // number the items
247 int n = 1;
248         for (multiset<Todo>::iterator i = out.begin(); i != out.end(); ++i, ++n) {
249         Todo &t = const_cast<Todo&>(*i);
250                
251                 t.index = n;
252         }
253 }
254
255 void TodoDB::fixParents(multiset<Todo> &todo, Todo *parent) {
256         for (multiset<Todo>::iterator i = todo.begin(); i != todo.end(); ++i) {
257         Todo &todo = (Todo&)*i;
258
259                 if (parent)
260                         todo.parent = parent;
261                 fixParents(*todo.child, &todo);
262         }
263 }
264
265 void TodoDB::load(string const &file) {
266 Loader loader;
267 string lastError;
268
269         setDirty(false);
270
271         stat(file.c_str(), &_stat);
272         if (options.timeout && options.mode == View && (time(0) - _stat.st_atime < options.timeoutseconds)) {
273                 if (options.verbose)
274                         cout << "todo: database not displayed due to timeout" << endl;
275                 return;
276         }
277
278         options.timeout = false;
279
280         filename = file;
281
282         /* Get the base name */
283 string::size_type pos = filename.rfind("/");
284        
285         if (pos > 0 && pos != string::npos)
286                 basepath = filename.substr(0, pos);
287
288 ifstream in(file.c_str());
289
290         statSuccessful = false;
291         if (in.bad()||in.fail()||in.eof()) throw quit();
292         statSuccessful = true;
293
294         in.close();
295
296         loader = getLoaders();
297
298         for (vector<string>::iterator i = options.loaders.begin(); i != options.loaders.end(); ++i) {
299                 if (options.verbose > 1)
300                         cout << "todo: trying '" << (*i) << "' loader" << endl;
301                 if (loader.find(*i) == loader.end())
302                         throw exception("couldn't find loader for '" + *i + "'");
303                 try {
304                         if (loader[*i](*this, file)) {
305                                 if (options.verbose > 1)
306                                         cout << "todo: loaded database successfully with '" << (*i) << "' loader" << endl;
307                                 triggerEvent("load");
308                                 return;
309                         }
310                 } catch (std::exception &e) {
311                         lastError = e.what();
312                 }
313         }
314         throw exception("no database loaders for database format or database corrupt (last error was '" + lastError + "'");
315 }
316
317 void TodoDB::save(multiset<Todo> const &todo, ostream &of, int ind) {
318
319         for (multiset<Todo>::const_iterator i = todo.begin(); i != todo.end(); i++)
320         {
321                 cerr << "saving: " << i->text << endl;
322                 if (i->type == Todo::Link) {
323                         of  << string(ind * 4, ' ') << "<link"
324                             << " filename=\"" << i->todofile << "\""
325                                 << " priority=\"" << symbolisePriority(i->priority) << "\""
326                                 << " time=\"" << i->added << "\""
327                                 << "/>" << endl;
328
329                         if (i->db)
330                                 i->db->save(i->todofile);
331
332                         /* Restore the TODODB environment variable. */
333 string envar = "TODODB=" + filename;
334
335                         //setenv("TODODB", options.database.c_str(), 1);
336                         putenv(strdup(const_cast<char*>(envar.c_str())));
337
338                 }
339                 else {
340                         of      << string(ind * 4, ' ') << "<note"
341                                 << " priority=\"" << symbolisePriority((*i).priority) << "\""
342                                 << " time=\"" << (*i).added << "\"";
343                         if ((*i).done) {
344                                 of      << " done=\"" << (*i).doneTime<< "\"";
345                         }
346                         of << ">" << endl;
347                         of << string((ind + 1) * 4, ' ');
348                         of << htmlify((*i).text) << endl;
349                         if ((*i).comment != "") {
350                                 of << "<comment>" << endl;
351                                 of << string((ind + 2) * 4, ' ');
352                                 of << htmlify((*i).comment) << endl;
353                                 of << "</comment>" << endl;
354                         }
355                         save(*i->child, of, ind + 1);
356                         of << string(ind * 4, ' ');
357                         of << "</note>" << endl;
358                 }
359         }
360 }
361
362 void TodoDB::save(string const &file) {
363         // Do backups
364         if (dirty && options.backups) {
365         string newname;
366                 for (int i = options.backups - 1; i > 0; i--) {
367                         newname = file + "." + stringify(i + 1);
368                         if (options.verbose > 1)
369                                 cout << "todo: renaming " << file << "." << i  << " to " << file << "." << i + 1 << endl;
370                         chmod(newname.c_str(), 0600);
371                         ::unlink(newname.c_str());
372                         rename((file + "." + stringify(i)).c_str(), newname.c_str());
373                         chmod(newname.c_str(), 0400);
374                 }
375                 if (options.verbose > 1)
376                         cout << "todo: renaming " << file << " to " << file << ".1" << endl;
377                 newname = file + ".1";
378                 chmod(newname.c_str(), 0600);
379                 ::unlink(newname.c_str());
380                 rename(file.c_str(), newname.c_str());
381                 chmod(newname.c_str(), 0400);
382         }
383         if (todo.size() || titleText != "") {
384         Saver saver = getSavers();
385         string lastError;
386
387                 if (dirty && options.verbose > 1)
388                         cout << "todo: saving to database '" << file << "'" << endl;
389
390                 for (vector<string>::iterator i = options.loaders.begin(); i != options.loaders.end(); ++i) {
391                         if (dirty && options.verbose > 1)
392                                 cout << "todo: trying '" << (*i) << "' saver" << endl;
393                         if (saver.find(*i) == saver.end())
394                                 throw exception("couldn't find saver for '" + *i + "'");
395                         try {
396                                 if (saver[*i](*this, file)) {
397                                         // Preserve ownership and mode
398                                         if (statSuccessful) {
399                                                 if (options.verbose > 1)
400                                                         cout << "todo: preserving attributes" << endl;
401                                                 chmod(file.c_str(), _stat.st_mode);
402                                                 chown(file.c_str(), _stat.st_uid, _stat.st_gid);
403                                         } else {
404                                                 triggerEvent("create");
405                                                 if (options.paranoid) {
406                                                         if (options.verbose > 1)
407                                                                 cout << "todo: paranoia check" << endl;
408                                                         stat(file.c_str(), &_stat);
409                   &nb