root/todo/trunk/src/support.cc

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

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

Line 
1 #include <unistd.h>
2 #include <cstdlib>
3 #include <cstring>
4 #include <cstdio>
5 #include <sstream>
6 #include <stdexcept>
7 #include "support.h"
8 #include "TodoDB.h"
9 #include "Strings.h"
10 #include "CommandArgs.h"
11 #include "todoterm.h"
12 #include "config.h"
13 #include "todorl.h"
14
15 using namespace str;
16 static bool nukeonhit = false;
17
18 Options options;
19
20 Options::Options() :
21         verbose(0), purgeage(0), mono(false), paranoid(false), global(false),
22         summary(false), timeout(false), comment(false),
23         database(".todo"),
24         priority(Todo::None),
25         mode(TodoDB::View),
26         backups(0), timeoutseconds(0) {
27
28         // default formatting strings
29         format["date"] = "%c";
30         format["display"] = "%4>%i%f%[info]%2n.%[priority]%+1T\n";
31         format["comment-display"] = "%4>%i%f%[info]%2n.%[priority]%+1T\n%+1i%[comment](Comment: %C)\n";
32         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";
33         format["generated"] = "%2>%i-%+1T\n%+1i(added %c, %d, priority %p)\n\n";
34         format["verbose-generated"] = "%2>%i- %+1T\n\n%+1iAdded: %c  Completed: %d\n%+1iDuration: %D  Priority: %p\n\n\n";
35
36         loaders.push_back("xml");
37         loaders.push_back("binary");
38
39         if (getenv("HOME"))
40                 globaldatabase = string(getenv("HOME")) + "/.todo_global";
41         else
42                 globaldatabase = "";
43
44         filename = "";
45
46         columns = getWidth();
47 }
48
49 Options::Filter::Filter() {
50         // defaults for filter
51         childrendir = Equal;
52         children = false;
53         prioritydir = Equal;
54         priority = Todo::None;
55         donedir = Negative;
56         done = true;
57         show = Equal;
58 }
59
60 void parseSort(string const &arg) {
61 vector<string> keys = str::split(",", arg);
62 struct {
63         const char *str;
64         int key;
65 } table[] = {
66         { "created", Options::Sort::Created },
67         { "completed", Options::Sort::Completed },
68         { "text", Options::Sort::Text },
69         { "priority", Options::Sort::Priority },
70         { "duration", Options::Sort::Duration },
71         { "done", Options::Sort::Done },
72         { "none", Options::Sort::None },
73         { 0, 0 }
74 };
75         options.sort.clear();
76
77         for (vector<string>::iterator i = keys.begin(); i != keys.end(); ++i) {
78         string p = *i;
79         Options::Sort sort;
80
81                 if (p[0] == '-') {
82                         sort.dir = Options::Negative;
83                         p = p.substr(1);
84                 } else {
85                         sort.dir = Options::Equal;
86                         if (p[0] == '+') p = p.substr(1);
87                 }
88         int j;
89                 for (j = 0; table[j].str != 0; ++j)
90                         if (p == table[j].str) {
91                                 sort.key = (Options::Sort::Key)table[j].key;
92                                 break;
93                         }
94                 if (table[j].str == 0)
95                         throw runtime_error("unknown sort key '" + p + "'");
96                 options.sort.push_back(sort);
97         }
98 }
99
100 void parseFilter(TodoDB &todo, string const &arg) {
101 vector<string> out;
102
103         out = split(",", arg);
104         for (vector<string>::iterator i = out.begin(); i != out.end(); i++) {
105         Options::Dir dir = Options::Equal;
106         string str = *i;
107
108                 // Search
109                 if (str[0] == '/') {
110                         options.filter.search = str.c_str() + 1;
111                 } else {
112                         if (str[0] == '-' || str[0] == '+' || str[0] == '=') {
113                                 if (str[0] == '-')
114                                         dir = Options::Negative;
115                                 else
116                                 if (str[0] == '+')
117                                         dir = Options::Positive;
118                                 else
119                                         dir = Options::Equal;
120                                 // singular '-' or '+' sets default display mode
121                                 if (str.size() == 1) {
122                                         options.filter.show = dir;
123                                         continue;
124                                 }
125                                 str = str.substr(1);
126                         }
127                         // must be children?
128                         if (str == "children") {
129                                 options.filter.children = true;
130                                 options.filter.childrendir = dir;
131                         } else
132                         // ...or done?
133                         if (str == "done") {
134                                 options.filter.done = true;
135                                 options.filter.donedir = dir;
136                         } else
137                         // catch-all, show everything
138                         if (str == "all") {
139                                 options.filter.done = true;
140                                 options.filter.donedir = Options::Positive;
141                                 options.filter.children = true;
142                                 options.filter.childrendir = Options::Positive;
143                         } else
144                         if (isdigit(str[0])) {
145                         vector<string> indices = todo.getIndexList(str);
146
147                                 for (vector<string>::iterator i = indices.begin(); i != indices.end(); ++i) {
148                                         options.filter.item[*i] = dir;
149                                         if (dir != Options::Negative)
150                                                 options.filter.show = Options::Negative;
151                                 }
152                         } else
153                                 // is it a priority?
154                                 try {
155                                 Todo::Priority n = desymbolisePriority(symbolisePriority(str));
156
157                                         options.filter.priority = n;
158                                         options.filter.prioritydir = dir;
159                                 } catch (logic_error&) {
160                                         throw runtime_error("unexpected symbol '" + str + "' in filter expression");
161                                 }
162                 }
163         }
164 }
165
166 static void mergeVectors(vector<string> &out, vector<string> const &in) {
167         for (vector<string>::const_iterator i = in.begin(); i != in.end(); ++i)
168                 out.push_back(*i);
169 }
170
171 /**
172         Parse command line arguments into an associative array of key/value
173         pairs.
174
175         @param  argc    argc from main()
176         @param  argv    argv from main()
177         @return                 key/value pairs
178 */
179 void parseArgs(TodoDB &todo, int argc, char const **argv) {
180 CommandArgs args;
181 string myname = argv[0];
182
183         // detect default mode from command name
184         if (myname.rfind("tda") == myname.size() - 3)
185                 options.mode = TodoDB::Add;
186
187         if (myname.rfind("tde") == myname.size() - 3)
188                 options.mode = TodoDB::Edit;
189
190         if (myname.rfind("tdr") == myname.size() - 3)
191                 options.mode = TodoDB::Remove;
192
193         if (myname.rfind("tdd") == myname.size() - 3)
194                 options.mode = TodoDB::Done;
195
196         if (myname.rfind("tdl") == myname.size() - 3)
197                 options.mode = TodoDB::Link;
198
199 enum { Help = -100, Remove, Version, Title, Colour, Mono, ForceColour,
200         Database, GlobalDatabase, DateFormat, DisplayFormat,
201         VerboseDisplayFormat, VerboseGeneratedFormat,
202         GeneratedFormat, Sort, Paranoid, DatabaseLoaders, Backup,
203         Timeout, Format, UseFormat, DumpConfig, Exec, Echo, Stats, Purge };
204
205         args.addArgument('a', "add", CommandArgs::Optional);
206         args.setHelp('a', "Add a note (will prompt for a note if one is not supplied).");
207         args.addArgument('l', "link", CommandArgs::Required);
208         args.setHelp('l', "Links another todo file to this one.");
209         args.addArgument('g', "graft", CommandArgs::Required);
210         args.setHelp('g', "In conjunction with --add or --link, graft the new item to the specified item.");
211         args.addArgument('R', "reparent", CommandArgs::Required);
212         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");
213         args.addArgument('p', "priority", CommandArgs::Required);
214         args.setHelp('p', "In conjunction with --add, set the default priority (default|veryhigh|high|medium|low|verylow).");
215         args.addArgument('e', "edit", CommandArgs::Required);
216         args.setHelp('e', "Edit the note that is indexed by the given number.");
217         args.addArgument(Remove, "remove", CommandArgs::Required);
218         args.setHelp(Remove, "Remove the note that is indexed by the given number.");
219         args.addArgument('d', "done", CommandArgs::Required);
220         args.setHelp('d', "Mark the specified notes (and all children) as done.");
221         args.addArgument('c', "comment");
222         args.setHelp('c', "Also manipulate finishing comment for item when viewing or marking as done.");
223         args.addArgument('D', "not-done", CommandArgs::Required);
224         args.setHelp('D', "Mark the specified notes (and all children) as not done.");
225         args.addArgument(GlobalDatabase, "global-database", CommandArgs::Required);
226         args.setHelp(GlobalDatabase, "Specify the database to use if the -G (--global) parameter is used.");
227         args.addArgument('G', "global");
228         args.setHelp('G', "Use the database specified by the --global-database option. Defaults to ~/.todo_global.");
229         args.addArgument(Database, "database", CommandArgs::Required);
230         args.setHelp(Database, "Change the database from the default (.todo) to the filename specified.");
231         args.addArgument('T', "TODO");
232         args.setHelp('T', "Generate a typical TODO output text file from a Todo DB.");
233         args.addArgument('A', "all");
234         args.setHelp('A', "Shortcut for the filter '+done,+children' to show all notes.");
235         args.addArgument('f', "filter", CommandArgs::Required);
236         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 '/<expression>'. 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.");
237         args.addArgument('v', "verbose");
238         args.setHelp('v', "Display verbosely.");
239         args.addArgument(Colour, "colour", CommandArgs::Required);
240         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 '+'.");
241         args.addArgument(Mono, "mono");
242         args.setHelp(Mono, "Disable all use of colour.");
243         args.addArgument(ForceColour, "force-colour");
244         args.setHelp(ForceColour, "Force use of colour.");
245         args.addArgument(Help, "help");
246         args.setHelp(Help, "Display this help.");
247         args.addArgument(Version, "version");
248         args.setHelp(Version, "Display version of todo.");
249         args.addArgument(Title, "title", CommandArgs::Optional);
250         args.setHelp(Title, "Set the title of this directory's todo notes.");
251         args.addArgument(DateFormat, "date-format", CommandArgs::Required);
252         args.setHelp(DateFormat, "Format the display of time values. The format is that used by strftime(3).");
253         args.addArgument(Format, "format", CommandArgs::Required);
254         args.setHelp(Format, "Specify a display format to use. ARG is in the form <key>=<format-string>.");
255         args.addArgument(UseFormat, "use-format", CommandArgs::Required);
256         args.setHelp(UseFormat, "Map one format string to another (eg. --use-format display=my-own-format).");
257         args.addArgument(DumpConfig, "dump-config");
258         //      Individual formatting strings are deprecated
259         args.addArgument(DisplayFormat, "display-format", CommandArgs::Required);
260         //args.setHelp(DisplayFormat, "Specify the format of items when displaying them to the console.");
261         args.addArgument(VerboseDisplayFormat, "verbose-display-format", CommandArgs::Required);
262         //args.setHelp(VerboseDisplayFormat, "As with --display-format but used when --verbose (-v) is specified.");
263         args.addArgument(GeneratedFormat, "generated-format", CommandArgs::Required);
264         //args.setHelp(GeneratedFormat, "Specify the format of items when generating a TODO file from the database.");
265         args.addArgument(VerboseGeneratedFormat, "verbose-generated-format", CommandArgs::Required);
266         //args.setHelp(VerboseGeneratedFormat, "As with --generated-format but used when --verbose (-v) is specified.");
267         args.addArgument(Sort, "sort", CommandArgs::Required);
268         args.setHelp(Sort, "Sort the database with the specified expression.");
269         args.addArgument(Paranoid, "paranoid");
270         args.setHelp(Paranoid, "Be paranoid about some settings, including permissions.");
271         args.addArgument(DatabaseLoaders, "database-loaders", CommandArgs::Required);
272         args.setHelp(DatabaseLoaders, "Specify order of database format loaders to use when loading and saving (defaults to 'xml').");
273         args.addArgument(Backup, "backup", CommandArgs::Optional);
274         args.setHelp(Backup, "Enable backups of the database. The optional argument is the number of backups to keep (defaults to 1).");
275         args.addArgument('s', "summary");
276         args.setHelp('s', "Toggle summary mode.");
277         args.addArgument(Timeout, "timeout", CommandArgs::Optional);
278         args.setHelp(Timeout, "Set the number of seconds between database displays and/or conform to this setting (see man page for more information).");
279         args.addArgument(Purge, "purge", CommandArgs::Optional);
280         args.setHelp(Purge, "Purge items marked as done. Optionally only purge completed items older than ARG days.");
281         args.addArgument(Exec, "exec", CommandArgs::Required);
282         args.addArgument(Echo, "echo", CommandArgs::Required);
283         args.addArgument(Stats, "stats");
284
285         for (CommandArgs::iterator i = args.begin(argc, argv); !(i == args.end()); i++)
286                 switch (i.option()) {
287                         case Help :
288                                 cout << "usage: " << myname << " [<arguments>]" << endl
289                                         << "Where <arguments> can be any of the following:" << endl;
290                                 args.displayHelp(cout, options.columns);
291                                 cout << endl
292                                         << "In addition, there are five convenience symlinks. These are 'tda', 'tdr'," << endl
293                                         << "'tdd', 'tde', and 'tdl'. For 'tde', 'tdd' and 'tdr' supply an index to edit," << endl
294                                         << "mark done and remove respectively. For 'tda' supply the text of the todo item" << endl
295                                         << "item and optionally the priority. For 'tdl' supply the path to another todo" << endl
296                                         << "file to link in to the current todo file." << endl
297                                         << "eg. tde 1" << endl;
298                                 exit(0);
299                         break;
300                         case Version :
301                                 cout << VERSION << endl;
302                                 exit(0);
303                         break;
304                         case Title :
305                                 if (i.parameter()) options.text = i.parameter();
306                                 options.mode = TodoDB::Title;
307                         break;
308                         case DateFormat :
309                                 options.format["date"] = i.parameter();
310                         break;
311                         case Mono :
312                                 options.mono = true;
313                         break;
314                         case Database :
315                                 options.database = i.parameter();
316                         break;
317                         case Sort :
318                                 parseSort(i.parameter());
319                         break;
320                         case GlobalDatabase :
321                                 options.globaldatabase = i.parameter();
322                         break;
323                         case Backup :
324                                 if (i.parameter()) {
325                                         options.backups = destringify<int>(i.parameter());
326                                         if (options.backups <= 0)
327                                                 throw runtime_error("can't specify backup revisions <= 0");
328                                 } else
329                                         options.backups = 1;
330                         break;
331                         case Timeout :
332                                 if (i.parameter()) {
333                                         options.timeoutseconds = destringify<int>(i.parameter());
334                                 } else {
335                                         if (options.timeoutseconds == 0) options.timeoutseconds = 10;
336                                         options.timeout = true;
337                                 }
338                         break;
339                         case Purge :
340                                 if (i.parameter())
341                                         options.purgeage = destringify<int>(i.parameter());
342                                 options.mode = TodoDB::Purge;
343                         break;
344                         case Exec :
345                                 if (options.verbose > 1)
346                                         cout << "todo: executing '" << i.parameter() << "'" << endl;
347                                 system(i.parameter());
348                         break;
349                         case Echo :
350                                 cout << i.parameter() << endl;
351                         break;
352                         case 's' :
353                                 options.summary = !options.summary;
354                         break;
355                         case 'G' :
356                                 options.global = true;
357                         break;
358                         case Paranoid :
359                                 options.paranoid = true;
360                         break;
361                         case DatabaseLoaders :
362                                 options.loaders = str::split(",", i.parameter());
363                         break;
364                         case Colour : {
365                         vector<string> colours = str::split(",", i.parameter());
366
367                                 for (vector<string>::iterator j = colours.begin(); j != colours.end(); ++j) {
368                                 vector<string> component = str::split("=", *j);
369                                         if (component.size() != 2)
370                                                 throw runtime_error(string("colour values should be in the format <item>=<colour>, not '") + i.parameter() + "'");
371                                         todo.setColour(component[0], component[1]);
372                                 }
373                         }
374                         break;
375                         case ForceColour :
376                                 term::forceColour(true);
377                         break;
378                         case Format : {
379                         vector<string> comp;
380                        
381                                 if (!strchr(i.parameter(), '='))
382                                         throw runtime_error("invalid usage of --format");
383                         string tmp = i.parameter();
384                                 comp.push_back(tmp.substr(0, strchr(i.parameter(), '=') - i.parameter()));
385                                 comp.push_back(tmp.substr(strchr(i.parameter(), '=') - i.parameter() + 1));
386                                 options.format[comp[0]] = comp[1];
387                         }
388                         break;
389                         case UseFormat : {
390                         vector<string> mapping = str::split("=", i.parameter());
391                                 if (mapping.size() != 2)
392                                         throw runtime_error("invalid usage of --use-format");
393                                 if (options.format.find(mapping[1]) == options.format.end() ||
394                                         options.format.find(mapping[1]) == options.format.end())
395                                         throw runtime_error("no such format '" + mapping[1] + "'");
396                                 options.format[mapping[0]] = options<