From dd423d315b17c98df67db5324cc758c7e122246a Mon Sep 17 00:00:00 2001 From: Paul Beckingham Date: Sat, 30 Jan 2010 23:39:51 -0500 Subject: [PATCH] Enhancement - Hooks - Implemented lots of command hooks. --- src/Context.cpp | 4 +- src/command.cpp | 2101 ++++++++++++++++++++++--------------------- src/custom.cpp | 894 ++++++++++--------- src/edit.cpp | 75 +- src/import.cpp | 121 +-- src/report.cpp | 2274 ++++++++++++++++++++++++----------------------- 6 files changed, 2818 insertions(+), 2651 deletions(-) diff --git a/src/Context.cpp b/src/Context.cpp index 4617fd3bf..3f64d66e1 100644 --- a/src/Context.cpp +++ b/src/Context.cpp @@ -248,7 +248,9 @@ int Context::dispatch (std::string &out) else if (cmd.validCustom (cmd.command)) { if (!inShadow) tdb.gc (); rc = handleCustomReport (cmd.command, out); } // If the command is not recognized, display usage. - else { rc = shortUsage (out); } + else { hooks.trigger ("pre-usage-command"); + rc = shortUsage (out); + hooks.trigger ("post-usage-command"); } // Only update the shadow file if such an update was not suppressed (shadow), if (cmd.isWriteCommand () && !inShadow) diff --git a/src/command.cpp b/src/command.cpp index 7e7ec1940..90916054d 100644 --- a/src/command.cpp +++ b/src/command.cpp @@ -53,6 +53,8 @@ extern Context context; //////////////////////////////////////////////////////////////////////////////// int handleAdd (std::string &outs) { + int rc = 0; + if (context.hooks.trigger ("pre-add-command")) { std::stringstream out; @@ -118,99 +120,106 @@ int handleAdd (std::string &outs) context.hooks.trigger ("post-add-command"); } - return 0; + return rc; } //////////////////////////////////////////////////////////////////////////////// int handleProjects (std::string &outs) { int rc = 0; - std::stringstream out; - context.filter.push_back (Att ("status", "pending")); - - std::vector tasks; - context.tdb.lock (context.config.getBoolean ("locking")); - int quantity = context.tdb.loadPending (tasks, context.filter); - context.tdb.commit (); - context.tdb.unlock (); - - // Scan all the tasks for their project name, building a map using project - // names as keys. - std::map unique; - std::map high; - std::map medium; - std::map low; - std::map none; - std::string project; - std::string priority; - foreach (t, tasks) + if (context.hooks.trigger ("pre-projects-command")) { - project = t->get ("project"); - priority = t->get ("priority"); + std::stringstream out; + context.filter.push_back (Att ("status", "pending")); - unique[project] += 1; + std::vector tasks; + context.tdb.lock (context.config.getBoolean ("locking")); + int quantity = context.tdb.loadPending (tasks, context.filter); + context.tdb.commit (); + context.tdb.unlock (); - if (priority == "H") high[project] += 1; - else if (priority == "M") medium[project] += 1; - else if (priority == "L") low[project] += 1; - else none[project] += 1; - } - - if (unique.size ()) - { - // Render a list of project names from the map. - Table table; - table.addColumn ("Project"); - table.addColumn ("Tasks"); - table.addColumn ("Pri:None"); - table.addColumn ("Pri:L"); - table.addColumn ("Pri:M"); - table.addColumn ("Pri:H"); - - if (context.config.getBoolean ("color") || - context.config.getBoolean ("_forcecolor")) + // Scan all the tasks for their project name, building a map using project + // names as keys. + std::map unique; + std::map high; + std::map medium; + std::map low; + std::map none; + std::string project; + std::string priority; + foreach (t, tasks) { - table.setColumnUnderline (0); - table.setColumnUnderline (1); - table.setColumnUnderline (2); - table.setColumnUnderline (3); - table.setColumnUnderline (4); - table.setColumnUnderline (5); + project = t->get ("project"); + priority = t->get ("priority"); + + unique[project] += 1; + + if (priority == "H") high[project] += 1; + else if (priority == "M") medium[project] += 1; + else if (priority == "L") low[project] += 1; + else none[project] += 1; } - table.setColumnJustification (1, Table::right); - table.setColumnJustification (2, Table::right); - table.setColumnJustification (3, Table::right); - table.setColumnJustification (4, Table::right); - table.setColumnJustification (5, Table::right); - - foreach (i, unique) + if (unique.size ()) { - int row = table.addRow (); - table.addCell (row, 0, i->first); - table.addCell (row, 1, i->second); - table.addCell (row, 2, none[i->first]); - table.addCell (row, 3, low[i->first]); - table.addCell (row, 4, medium[i->first]); - table.addCell (row, 5, high[i->first]); + // Render a list of project names from the map. + Table table; + table.addColumn ("Project"); + table.addColumn ("Tasks"); + table.addColumn ("Pri:None"); + table.addColumn ("Pri:L"); + table.addColumn ("Pri:M"); + table.addColumn ("Pri:H"); + + if (context.config.getBoolean ("color") || + context.config.getBoolean ("_forcecolor")) + { + table.setColumnUnderline (0); + table.setColumnUnderline (1); + table.setColumnUnderline (2); + table.setColumnUnderline (3); + table.setColumnUnderline (4); + table.setColumnUnderline (5); + } + + table.setColumnJustification (1, Table::right); + table.setColumnJustification (2, Table::right); + table.setColumnJustification (3, Table::right); + table.setColumnJustification (4, Table::right); + table.setColumnJustification (5, Table::right); + + foreach (i, unique) + { + int row = table.addRow (); + table.addCell (row, 0, i->first); + table.addCell (row, 1, i->second); + table.addCell (row, 2, none[i->first]); + table.addCell (row, 3, low[i->first]); + table.addCell (row, 4, medium[i->first]); + table.addCell (row, 5, high[i->first]); + } + + out << optionalBlankLine () + << table.render () + << optionalBlankLine () + << unique.size () + << (unique.size () == 1 ? " project" : " projects") + << " (" << quantity << (quantity == 1 ? " task" : " tasks") << ")" + << std::endl; + } + else + { + out << "No projects." + << std::endl; + + rc = 1; } - out << optionalBlankLine () - << table.render () - << optionalBlankLine () - << unique.size () - << (unique.size () == 1 ? " project" : " projects") - << " (" << quantity << (quantity == 1 ? " task" : " tasks") << ")" - << std::endl; - } - else { - out << "No projects." - << std::endl; - rc = 1; + outs = out.str (); + context.hooks.trigger ("post-projects-command"); } - outs = out.str (); return rc; } @@ -248,69 +257,75 @@ int handleCompletionProjects (std::string &outs) int handleTags (std::string &outs) { int rc = 0; - std::stringstream out; - context.filter.push_back (Att ("status", "pending")); - - std::vector tasks; - context.tdb.lock (context.config.getBoolean ("locking")); - int quantity = context.tdb.loadPending (tasks, context.filter); - context.tdb.commit (); - context.tdb.unlock (); - - // Scan all the tasks for their project name, building a map using project - // names as keys. - std::map unique; - foreach (t, tasks) + if (context.hooks.trigger ("pre-tags-command")) { - std::vector tags; - t->getTags (tags); + std::stringstream out; - foreach (tag, tags) - if (unique.find (*tag) != unique.end ()) - unique[*tag]++; - else - unique[*tag] = 1; - } + context.filter.push_back (Att ("status", "pending")); - if (unique.size ()) - { - // Render a list of tags names from the map. - Table table; - table.addColumn ("Tag"); - table.addColumn ("Count"); + std::vector tasks; + context.tdb.lock (context.config.getBoolean ("locking")); + int quantity = context.tdb.loadPending (tasks, context.filter); + context.tdb.commit (); + context.tdb.unlock (); - if (context.config.getBoolean ("color") || - context.config.getBoolean ("_forcecolor")) + // Scan all the tasks for their project name, building a map using project + // names as keys. + std::map unique; + foreach (t, tasks) { - table.setColumnUnderline (0); - table.setColumnUnderline (1); + std::vector tags; + t->getTags (tags); + + foreach (tag, tags) + if (unique.find (*tag) != unique.end ()) + unique[*tag]++; + else + unique[*tag] = 1; } - table.setColumnJustification (1, Table::right); - - foreach (i, unique) + if (unique.size ()) { - int row = table.addRow (); - table.addCell (row, 0, i->first); - table.addCell (row, 1, i->second); + // Render a list of tags names from the map. + Table table; + table.addColumn ("Tag"); + table.addColumn ("Count"); + + if (context.config.getBoolean ("color") || + context.config.getBoolean ("_forcecolor")) + { + table.setColumnUnderline (0); + table.setColumnUnderline (1); + } + + table.setColumnJustification (1, Table::right); + + foreach (i, unique) + { + int row = table.addRow (); + table.addCell (row, 0, i->first); + table.addCell (row, 1, i->second); + } + + out << optionalBlankLine () + << table.render () + << optionalBlankLine () + << unique.size () + << (unique.size () == 1 ? " tag" : " tags") + << " (" << quantity << (quantity == 1 ? " task" : " tasks") << ")" + << std::endl; + } + else + { + out << "No tags." + << std::endl; + rc = 1; } - out << optionalBlankLine () - << table.render () - << optionalBlankLine () - << unique.size () - << (unique.size () == 1 ? " tag" : " tags") - << " (" << quantity << (quantity == 1 ? " task" : " tasks") << ")" - << std::endl; + outs = out.str (); + context.hooks.trigger ("post-tags-command"); } - else { - out << "No tags." - << std::endl; - rc = 1; - } - - outs = out.str (); return rc; } @@ -419,94 +434,105 @@ int handleCompletionIDs (std::string &outs) //////////////////////////////////////////////////////////////////////////////// void handleUndo () { - context.disallowModification (); + if (context.hooks.trigger ("pre-undo-command")) + { + context.disallowModification (); - context.tdb.lock (context.config.getBoolean ("locking")); - context.tdb.undo (); - context.tdb.unlock (); + context.tdb.lock (context.config.getBoolean ("locking")); + context.tdb.undo (); + context.tdb.unlock (); + + context.hooks.trigger ("post-undo-command"); + } } //////////////////////////////////////////////////////////////////////////////// int handleVersion (std::string &outs) { int rc = 0; - std::stringstream out; - // Create a table for the disclaimer. - int width = context.getWidth (); - Table disclaimer; - disclaimer.setTableWidth (width); - disclaimer.addColumn (" "); - disclaimer.setColumnWidth (0, Table::flexible); - disclaimer.setColumnJustification (0, Table::left); - disclaimer.addCell (disclaimer.addRow (), 0, - "Task may be copied only under the terms of the GNU General Public " - "License, which may be found in the task source kit."); + if (context.hooks.trigger ("pre-version-command")) + { + std::stringstream out; - // Create a table for the URL. - Table link; - link.setTableWidth (width); - link.addColumn (" "); - link.setColumnWidth (0, Table::flexible); - link.setColumnJustification (0, Table::left); - link.addCell (link.addRow (), 0, - "Documentation for task can be found using 'man task', 'man taskrc', " - "'man task-tutorial', 'man task-faq' or at http://taskwarrior.org"); + // Create a table for the disclaimer. + int width = context.getWidth (); + Table disclaimer; + disclaimer.setTableWidth (width); + disclaimer.addColumn (" "); + disclaimer.setColumnWidth (0, Table::flexible); + disclaimer.setColumnJustification (0, Table::left); + disclaimer.addCell (disclaimer.addRow (), 0, + "Task may be copied only under the terms of the GNU General Public " + "License, which may be found in the task source kit."); - Color bold ("bold"); + // Create a table for the URL. + Table link; + link.setTableWidth (width); + link.addColumn (" "); + link.setColumnWidth (0, Table::flexible); + link.setColumnJustification (0, Table::left); + link.addCell (link.addRow (), 0, + "Documentation for task can be found using 'man task', 'man taskrc', " + "'man task-tutorial', 'man task-faq' or at http://taskwarrior.org"); - out << std::endl - << ((context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor")) - ? bold.colorize (PACKAGE) - : PACKAGE) - << " " - << ((context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor")) - ? bold.colorize (VERSION) - : VERSION) - << " built for " + Color bold ("bold"); + + out << std::endl + << ((context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor")) + ? bold.colorize (PACKAGE) + : PACKAGE) + << " " + << ((context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor")) + ? bold.colorize (VERSION) + : VERSION) + << " built for " #if defined (DARWIN) - << "darwin" + << "darwin" #elif defined (SOLARIS) - << "solaris" + << "solaris" #elif defined (CYGWIN) - << "cygwin" + << "cygwin" #elif defined (OPENBSD) - << "openbsd" + << "openbsd" #elif defined (HAIKU) - << "haiku" + << "haiku" #elif defined (FREEBSD) - << "freebsd" + << "freebsd" #elif defined (LINUX) - << "linux" + << "linux" #else - << "unknown" + << "unknown" #endif #ifdef HAVE_LIBNCURSES - << "-ncurses" + << "-ncurses" #endif #ifdef HAVE_LIBREADLINE - << "-readline" + << "-readline" #endif #ifdef HAVE_LIBLUA - << "-lua" + << "-lua" #endif - << std::endl - << "Copyright (C) 2006 - 2010 P. Beckingham, F. Hernandez." - << std::endl + << std::endl + << "Copyright (C) 2006 - 2010 P. Beckingham, F. Hernandez." + << std::endl #ifdef HAVE_LIBLUA - << "Portions of this software Copyright (C) 1994 – 2008 Lua.org, PUC-Rio." - << std::endl + << "Portions of this software Copyright (C) 1994 – 2008 Lua.org, PUC-Rio." + << std::endl #endif - << disclaimer.render () - << link.render () - << std::endl; + << disclaimer.render () + << link.render () + << std::endl; + + outs = out.str (); + context.hooks.trigger ("post-version-command"); + } - outs = out.str (); return rc; } @@ -514,322 +540,328 @@ int handleVersion (std::string &outs) int handleConfig (std::string &outs) { int rc = 0; - std::stringstream out; - // Support: - // task config name value # set name to value - // task config name "" # set name to blank - // task config name # remove name - if (context.args.size () >= 2) + if (context.hooks.trigger ("pre-config-command")) { - std::string name = context.args[1]; - std::string value = ""; + std::stringstream out; - if (context.args.size () >= 3) - value = context.args[2]; - - if (name != "") + // Support: + // task config name value # set name to value + // task config name "" # set name to blank + // task config name # remove name + if (context.args.size () >= 2) { - bool change = false; + std::string name = context.args[1]; + std::string value = ""; - // Read .taskrc (or equivalent) - std::string contents; - File::read (context.config.original_file, contents); - - // task config name value - // task config name "" if (context.args.size () >= 3) + value = context.args[2]; + + if (name != "") { - // Find existing entry & overwrite - std::string::size_type pos = contents.find (name + "="); - if (pos != std::string::npos) + bool change = false; + + // Read .taskrc (or equivalent) + std::string contents; + File::read (context.config.original_file, contents); + + // task config name value + // task config name "" + if (context.args.size () >= 3) { + // Find existing entry & overwrite + std::string::size_type pos = contents.find (name + "="); + if (pos != std::string::npos) + { + std::string::size_type eol = contents.find_first_of ("\r\f\n", pos); + if (eol == std::string::npos) + throw std::string ("Cannot find EOL after entry '") + name + "'"; + + if (confirm (std::string ("Are you sure you want to change the value of '") + + name + + "' from '" + + context.config.get(name) + + "' to '" + + value + "'?")) + { + contents = contents.substr (0, pos) + + name + "=" + value + + contents.substr (eol); + change = true; + } + } + + // Not found, so append instead. + else + { + if (confirm (std::string ("Are you sure you want to add '") + name + "' with a value of '" + value + "'?")) + { + contents = contents + + "\n" + + name + "=" + value + + "\n"; + change = true; + } + } + } + + // task config name + else + { + // Remove name + std::string::size_type pos = contents.find (name + "="); + if (pos == std::string::npos) + throw std::string ("No entry named '") + name + "' found"; + std::string::size_type eol = contents.find_first_of ("\r\f\n", pos); if (eol == std::string::npos) throw std::string ("Cannot find EOL after entry '") + name + "'"; - if (confirm (std::string ("Are you sure you want to change the value of '") - + name - + "' from '" - + context.config.get(name) - + "' to '" - + value + "'?")) + if (confirm (std::string ("Are you sure you want to remove '") + name + "'?")) { - contents = contents.substr (0, pos) - + name + "=" + value - + contents.substr (eol); + contents = contents.substr (0, pos) + contents.substr (eol + 1); change = true; } } - // Not found, so append instead. + // Write .taskrc (or equivalent) + if (change) + { + File::write (context.config.original_file, contents); + out << "Config file " + << context.config.original_file.data + << " modified." + << std::endl; + } else - { - if (confirm (std::string ("Are you sure you want to add '") + name + "' with a value of '" + value + "'?")) - { - contents = contents - + "\n" - + name + "=" + value - + "\n"; - change = true; - } - } - } - - // task config name - else - { - // Remove name - std::string::size_type pos = contents.find (name + "="); - if (pos == std::string::npos) - throw std::string ("No entry named '") + name + "' found"; - - std::string::size_type eol = contents.find_first_of ("\r\f\n", pos); - if (eol == std::string::npos) - throw std::string ("Cannot find EOL after entry '") + name + "'"; - - if (confirm (std::string ("Are you sure you want to remove '") + name + "'?")) - { - contents = contents.substr (0, pos) + contents.substr (eol + 1); - change = true; - } - } - - // Write .taskrc (or equivalent) - if (change) - { - File::write (context.config.original_file, contents); - out << "Config file " - << context.config.original_file.data - << " modified." - << std::endl; + out << "No changes made." << std::endl; } else - out << "No changes made." << std::endl; + throw std::string ("Specify the name of a config variable to modify."); + + outs = out.str (); + return rc; + } + + // No arguments - display config values instead. + int width = context.getWidth (); + + std::vector all; + context.config.all (all); + + // Create a table for output. + Table table; + table.setTableWidth (width); + table.setDateFormat (context.config.get ("dateformat")); + table.addColumn ("Config variable"); + table.addColumn ("Value"); + + if (context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor")) + { + table.setColumnUnderline (0); + table.setColumnUnderline (1); } else - throw std::string ("Specify the name of a config variable to modify."); + table.setTableDashedUnderline (); - outs = out.str (); - return rc; - } + table.setColumnWidth (0, Table::minimum); + table.setColumnWidth (1, Table::flexible); + table.setColumnJustification (0, Table::left); + table.setColumnJustification (1, Table::left); + table.sortOn (0, Table::ascendingCharacter); - // No arguments - display config values instead. - int width = context.getWidth (); - - std::vector all; - context.config.all (all); - - // Create a table for output. - Table table; - table.setTableWidth (width); - table.setDateFormat (context.config.get ("dateformat")); - table.addColumn ("Config variable"); - table.addColumn ("Value"); - - if (context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor")) - { - table.setColumnUnderline (0); - table.setColumnUnderline (1); - } - else - table.setTableDashedUnderline (); - - table.setColumnWidth (0, Table::minimum); - table.setColumnWidth (1, Table::flexible); - table.setColumnJustification (0, Table::left); - table.setColumnJustification (1, Table::left); - table.sortOn (0, Table::ascendingCharacter); - - foreach (i, all) - { - std::string value = context.config.get (*i); - int row = table.addRow (); - table.addCell (row, 0, *i); - table.addCell (row, 1, value); - } - - Color bold ("bold"); - - out << std::endl - << table.render () - << std::endl; - - // Complain about configuration variables that are not recognized. - // These are the regular configuration variables. - // Note that there is a leading and trailing space, to make searching easier. - std::string recognized = - " annotations blanklines bulk calendar.details calendar.details.report " - "calendar.holidays calendar.legend color color.active color.due " - "color.overdue color.pri.H color.pri.L color.pri.M color.pri.none " - "color.recurring color.tagged color.footnote color.header color.debug " - "color.alternate color.calendar.today color.calendar.due " - "color.calendar.overdue color.calendar.weekend color.calendar.holiday " - "color.calendar.weeknumber confirmation curses data.location dateformat " - "dateformat.holiday dateformat.report debug default.command " - "default.priority default.project defaultwidth due locale " - "displayweeknumber echo.command fontunderline locking monthsperline nag " - "next project shadow.command shadow.file shadow.notify weekstart editor " - "import.synonym.id import.synonym.uuid complete.all.projects " - "complete.all.tags search.case.sensitive hooks " -#ifdef FEATURE_SHELL - "shell.prompt " -#endif - "import.synonym.status import.synonym.tags import.synonym.entry " - "import.synonym.start import.synonym.due import.synonym.recur " - "import.synonym.end import.synonym.project import.synonym.priority " - "import.synonym.fg import.synonym.bg import.synonym.description "; - - // This configuration variable is supported, but not documented. It exists - // so that unit tests can force color to be on even when the output from task - // is redirected to a file, or stdout is not a tty. - recognized += "_forcecolor "; - - std::vector unrecognized; - foreach (i, all) - { - // Disallow partial matches by tacking a leading an trailing space on each - // variable name. - std::string pattern = " " + *i + " "; - if (recognized.find (pattern) == std::string::npos) - { - // These are special configuration variables, because their name is - // dynamic. - if (i->substr (0, 14) != "color.keyword." && - i->substr (0, 14) != "color.project." && - i->substr (0, 10) != "color.tag." && - i->substr (0, 8) != "holiday." && - i->substr (0, 7) != "report." && - i->substr (0, 6) != "alias." && - i->substr (0, 5) != "hook.") - { - unrecognized.push_back (*i); - } - } - } - - if (unrecognized.size ()) - { - out << "Your .taskrc file contains these unrecognized variables:" - << std::endl; - - foreach (i, unrecognized) - out << " " << *i << std::endl; - - out << std::endl; - } - - out << context.config.checkForDeprecatedColor (); - // TODO Check for referenced but missing theme files. - // TODO Check for referenced but missing string files. - // TODO Check for referenced but missing tips files. - - // Check for referenced but missing hook scripts. -#ifdef HAVE_LIBLUA - std::vector missing_scripts; - foreach (i, all) - { - if (i->substr (0, 5) == "hook.") + foreach (i, all) { std::string value = context.config.get (*i); - Nibbler n (value); + int row = table.addRow (); + table.addCell (row, 0, *i); + table.addCell (row, 1, value); + } - // : [, ...] - while (!n.depleted ()) + Color bold ("bold"); + + out << std::endl + << table.render () + << std::endl; + + // Complain about configuration variables that are not recognized. + // These are the regular configuration variables. + // Note that there is a leading and trailing space, to make searching easier. + std::string recognized = + " annotations blanklines bulk calendar.details calendar.details.report " + "calendar.holidays calendar.legend color color.active color.due " + "color.overdue color.pri.H color.pri.L color.pri.M color.pri.none " + "color.recurring color.tagged color.footnote color.header color.debug " + "color.alternate color.calendar.today color.calendar.due " + "color.calendar.overdue color.calendar.weekend color.calendar.holiday " + "color.calendar.weeknumber confirmation curses data.location dateformat " + "dateformat.holiday dateformat.report debug default.command " + "default.priority default.project defaultwidth due locale " + "displayweeknumber echo.command fontunderline locking monthsperline nag " + "next project shadow.command shadow.file shadow.notify weekstart editor " + "import.synonym.id import.synonym.uuid complete.all.projects " + "complete.all.tags search.case.sensitive hooks " +#ifdef FEATURE_SHELL + "shell.prompt " +#endif + "import.synonym.status import.synonym.tags import.synonym.entry " + "import.synonym.start import.synonym.due import.synonym.recur " + "import.synonym.end import.synonym.project import.synonym.priority " + "import.synonym.fg import.synonym.bg import.synonym.description "; + + // This configuration variable is supported, but not documented. It exists + // so that unit tests can force color to be on even when the output from task + // is redirected to a file, or stdout is not a tty. + recognized += "_forcecolor "; + + std::vector unrecognized; + foreach (i, all) + { + // Disallow partial matches by tacking a leading an trailing space on each + // variable name. + std::string pattern = " " + *i + " "; + if (recognized.find (pattern) == std::string::npos) { - std::string file; - std::string function; - if (n.getUntil (':', file) && - n.skip (':') && - n.getUntil (',', function)) + // These are special configuration variables, because their name is + // dynamic. + if (i->substr (0, 14) != "color.keyword." && + i->substr (0, 14) != "color.project." && + i->substr (0, 10) != "color.tag." && + i->substr (0, 8) != "holiday." && + i->substr (0, 7) != "report." && + i->substr (0, 6) != "alias." && + i->substr (0, 5) != "hook.") { - Path script (file); - if (!script.exists () || !script.readable ()) - missing_scripts.push_back (file); - - (void) n.skip (','); + unrecognized.push_back (*i); } } } - } - if (missing_scripts.size ()) - { - out << "Your .taskrc file contains these missing or unreadable hook scripts:" - << std::endl; + if (unrecognized.size ()) + { + out << "Your .taskrc file contains these unrecognized variables:" + << std::endl; - foreach (i, missing_scripts) - out << " " << *i << std::endl; + foreach (i, unrecognized) + out << " " << *i << std::endl; - out << std::endl; - } + out << std::endl; + } + + out << context.config.checkForDeprecatedColor (); + // TODO Check for referenced but missing theme files. + // TODO Check for referenced but missing string files. + // TODO Check for referenced but missing tips files. + + // Check for referenced but missing hook scripts. +#ifdef HAVE_LIBLUA + std::vector missing_scripts; + foreach (i, all) + { + if (i->substr (0, 5) == "hook.") + { + std::string value = context.config.get (*i); + Nibbler n (value); + + // : [, ...] + while (!n.depleted ()) + { + std::string file; + std::string function; + if (n.getUntil (':', file) && + n.skip (':') && + n.getUntil (',', function)) + { + Path script (file); + if (!script.exists () || !script.readable ()) + missing_scripts.push_back (file); + + (void) n.skip (','); + } + } + } + } + + if (missing_scripts.size ()) + { + out << "Your .taskrc file contains these missing or unreadable hook scripts:" + << std::endl; + + foreach (i, missing_scripts) + out << " " << *i << std::endl; + + out << std::endl; + } #endif - // Check for bad values in rc.annotations. - std::string annotations = context.config.get ("annotations"); - if (annotations != "full" && - annotations != "sparse" && - annotations != "none") - out << "Configuration error: annotations contains an unrecognized value '" - << annotations - << "'." - << std::endl; - - // Check for bad values in rc.calendar.details. - std::string calendardetails = context.config.get ("calendar.details"); - if (calendardetails != "full" && - calendardetails != "sparse" && - calendardetails != "none") - out << "Configuration error: calendar.details contains an unrecognized value '" - << calendardetails - << "'." - << std::endl; - - // Check for bad values in rc.calendar.holidays. - std::string calendarholidays = context.config.get ("calendar.holidays"); - if (calendarholidays != "full" && - calendarholidays != "sparse" && - calendarholidays != "none") - out << "Configuration error: calendar.holidays contains an unrecognized value '" - << calendarholidays - << "'." - << std::endl; - - // Check for bad values in rc.default.priority. - std::string defaultPriority = context.config.get ("default.priority"); - if (defaultPriority != "H" && - defaultPriority != "M" && - defaultPriority != "L" && - defaultPriority != "") - out << "Configuration error: default.priority contains an unrecognized value '" - << defaultPriority - << "'." - << std::endl; - - // Verify installation. This is mentioned in the documentation as the way to - // ensure everything is properly installed. - - if (all.size () == 0) { - out << "Configuration error: .taskrc contains no entries" - << std::endl; - rc = 1; - } - else - { - Directory location (context.config.get ("data.location")); - - if (location.data == "") - out << "Configuration error: data.location not specified in .taskrc " - "file." + // Check for bad values in rc.annotations. + std::string annotations = context.config.get ("annotations"); + if (annotations != "full" && + annotations != "sparse" && + annotations != "none") + out << "Configuration error: annotations contains an unrecognized value '" + << annotations + << "'." << std::endl; - if (! location.exists ()) - out << "Configuration error: data.location contains a directory name" - " that doesn't exist, or is unreadable." + // Check for bad values in rc.calendar.details. + std::string calendardetails = context.config.get ("calendar.details"); + if (calendardetails != "full" && + calendardetails != "sparse" && + calendardetails != "none") + out << "Configuration error: calendar.details contains an unrecognized value '" + << calendardetails + << "'." << std::endl; - } - outs = out.str (); + // Check for bad values in rc.calendar.holidays. + std::string calendarholidays = context.config.get ("calendar.holidays"); + if (calendarholidays != "full" && + calendarholidays != "sparse" && + calendarholidays != "none") + out << "Configuration error: calendar.holidays contains an unrecognized value '" + << calendarholidays + << "'." + << std::endl; + + // Check for bad values in rc.default.priority. + std::string defaultPriority = context.config.get ("default.priority"); + if (defaultPriority != "H" && + defaultPriority != "M" && + defaultPriority != "L" && + defaultPriority != "") + out << "Configuration error: default.priority contains an unrecognized value '" + << defaultPriority + << "'." + << std::endl; + + // Verify installation. This is mentioned in the documentation as the way + // to ensure everything is properly installed. + + if (all.size () == 0) + { + out << "Configuration error: .taskrc contains no entries" + << std::endl; + rc = 1; + } + else + { + Directory location (context.config.get ("data.location")); + + if (location.data == "") + out << "Configuration error: data.location not specified in .taskrc " + "file." + << std::endl; + + if (! location.exists ()) + out << "Configuration error: data.location contains a directory name" + " that doesn't exist, or is unreadable." + << std::endl; + } + + outs = out.str (); + context.hooks.trigger ("post-config-command"); + } return rc; } @@ -954,55 +986,61 @@ int handleDelete (std::string &outs) int handleStart (std::string &outs) { int rc = 0; - std::stringstream out; - context.disallowModification (); - - std::vector tasks; - context.tdb.lock (context.config.getBoolean ("locking")); - Filter filter; - context.tdb.loadPending (tasks, filter); - - // Filter sequence. - context.filter.applySequence (tasks, context.sequence); - - bool nagged = false; - foreach (task, tasks) + if (context.hooks.trigger ("pre-start-command")) { - if (! task->has ("start")) + std::stringstream out; + + context.disallowModification (); + + std::vector tasks; + context.tdb.lock (context.config.getBoolean ("locking")); + Filter filter; + context.tdb.loadPending (tasks, filter); + + // Filter sequence. + context.filter.applySequence (tasks, context.sequence); + + bool nagged = false; + foreach (task, tasks) { - char startTime[16]; - sprintf (startTime, "%u", (unsigned int) time (NULL)); - task->set ("start", startTime); + if (! task->has ("start")) + { + char startTime[16]; + sprintf (startTime, "%u", (unsigned int) time (NULL)); + task->set ("start", startTime); - context.tdb.update (*task); + context.tdb.update (*task); - if (context.config.getBoolean ("echo.command")) - out << "Started " + if (context.config.getBoolean ("echo.command")) + out << "Started " + << task->id + << " '" + << task->get ("description") + << "'" + << std::endl; + if (!nagged) + nagged = nag (*task); + } + else + { + out << "Task " << task->id << " '" << task->get ("description") - << "'" + << "' already started." << std::endl; - if (!nagged) - nagged = nag (*task); - } - else - { - out << "Task " - << task->id - << " '" - << task->get ("description") - << "' already started." - << std::endl; - rc = 1; + rc = 1; + } } + + context.tdb.commit (); + context.tdb.unlock (); + + outs = out.str (); + context.hooks.trigger ("post-start-command"); } - context.tdb.commit (); - context.tdb.unlock (); - - outs = out.str (); return rc; } @@ -1010,49 +1048,55 @@ int handleStart (std::string &outs) int handleStop (std::string &outs) { int rc = 0; - std::stringstream out; - context.disallowModification (); - - std::vector tasks; - context.tdb.lock (context.config.getBoolean ("locking")); - Filter filter; - context.tdb.loadPending (tasks, filter); - - // Filter sequence. - context.filter.applySequence (tasks, context.sequence); - - foreach (task, tasks) + if (context.hooks.trigger ("pre-stop-command")) { - if (task->has ("start")) - { - task->remove ("start"); - context.tdb.update (*task); + std::stringstream out; - if (context.config.getBoolean ("echo.command")) - out << "Stopped " + context.disallowModification (); + + std::vector tasks; + context.tdb.lock (context.config.getBoolean ("locking")); + Filter filter; + context.tdb.loadPending (tasks, filter); + + // Filter sequence. + context.filter.applySequence (tasks, context.sequence); + + foreach (task, tasks) + { + if (task->has ("start")) + { + task->remove ("start"); + context.tdb.update (*task); + + if (context.config.getBoolean ("echo.command")) + out << "Stopped " + << task->id + << " '" + << task->get ("description") + << "'" + << std::endl; + } + else + { + out << "Task " << task->id << " '" << task->get ("description") - << "'" + << "' not started." << std::endl; + rc = 1; + } } - else - { - out << "Task " - << task->id - << " '" - << task->get ("description") - << "' not started." - << std::endl; - rc = 1; - } + + context.tdb.commit (); + context.tdb.unlock (); + + outs = out.str (); + context.hooks.trigger ("post-stop-command"); } - context.tdb.commit (); - context.tdb.unlock (); - - outs = out.str (); return rc; } @@ -1060,144 +1104,157 @@ int handleStop (std::string &outs) int handleDone (std::string &outs) { int rc = 0; - int count = 0; - std::stringstream out; - std::vector tasks; - context.tdb.lock (context.config.getBoolean ("locking")); - Filter filter; - context.tdb.loadPending (tasks, filter); - - // Filter sequence. - std::vector all = tasks; - context.filter.applySequence (tasks, context.sequence); - - Permission permission; - if (context.sequence.size () > (size_t) context.config.getInteger ("bulk")) - permission.bigSequence (); - - bool nagged = false; - foreach (task, tasks) + if (context.hooks.trigger ("pre-done-command")) { - if (task->getStatus () == Task::pending) + int count = 0; + std::stringstream out; + + std::vector tasks; + context.tdb.lock (context.config.getBoolean ("locking")); + Filter filter; + context.tdb.loadPending (tasks, filter); + + // Filter sequence. + std::vector all = tasks; + context.filter.applySequence (tasks, context.sequence); + + Permission permission; + if (context.sequence.size () > (size_t) context.config.getInteger ("bulk")) + permission.bigSequence (); + + bool nagged = false; + foreach (task, tasks) { - Task before (*task); - - // Apply other deltas. - if (deltaDescription (*task)) - permission.bigChange (); - - deltaTags (*task); - deltaAttributes (*task); - deltaSubstitutions (*task); - - // Add an end date. - char entryTime[16]; - sprintf (entryTime, "%u", (unsigned int) time (NULL)); - task->set ("end", entryTime); - - // Change status. - task->setStatus (Task::completed); - - if (taskDiff (before, *task)) + if (task->getStatus () == Task::pending) { - context.hooks.setTaskId (task->id); - if (context.hooks.trigger ("pre-completed")) + Task before (*task); + + // Apply other deltas. + if (deltaDescription (*task)) + permission.bigChange (); + + deltaTags (*task); + deltaAttributes (*task); + deltaSubstitutions (*task); + + // Add an end date. + char entryTime[16]; + sprintf (entryTime, "%u", (unsigned int) time (NULL)); + task->set ("end", entryTime); + + // Change status. + task->setStatus (Task::completed); + + if (taskDiff (before, *task)) { - if (permission.confirmed (before, taskDifferences (before, *task) + "Proceed with change?")) + context.hooks.setTaskId (task->id); + if (context.hooks.trigger ("pre-completed")) { - context.tdb.update (*task); + if (permission.confirmed (before, taskDifferences (before, *task) + "Proceed with change?")) + { + context.tdb.update (*task); - if (context.config.getBoolean ("echo.command")) - out << "Completed " - << task->id - << " '" - << task->get ("description") - << "'" - << std::endl; + if (context.config.getBoolean ("echo.command")) + out << "Completed " + << task->id + << " '" + << task->get ("description") + << "'" + << std::endl; - ++count; + ++count; + } + + context.hooks.trigger ("post-completed"); } - - context.hooks.trigger ("post-completed"); + else + continue; } - else - continue; - } - updateRecurrenceMask (all, *task); - if (!nagged) - nagged = nag (*task); + updateRecurrenceMask (all, *task); + if (!nagged) + nagged = nag (*task); + } + else + out << "Task " + << task->id + << " '" + << task->get ("description") + << "' is not pending" + << std::endl; + rc = 1; } - else - out << "Task " - << task->id - << " '" - << task->get ("description") - << "' is not pending" + + if (count) + context.tdb.commit (); + + context.tdb.unlock (); + + if (context.config.getBoolean ("echo.command")) + out << "Marked " + << count + << " task" + << (count == 1 ? "" : "s") + << " as done" << std::endl; - rc = 1; + + outs = out.str (); + context.hooks.trigger ("post-done-command"); } - if (count) - context.tdb.commit (); - - context.tdb.unlock (); - - if (context.config.getBoolean ("echo.command")) - out << "Marked " - << count - << " task" - << (count == 1 ? "" : "s") - << " as done" - << std::endl; - - outs = out.str (); return rc; } //////////////////////////////////////////////////////////////////////////////// int handleExport (std::string &outs) { - std::stringstream out; + int rc = 0; - // Deliberately no 'id'. - out << "'uuid'," - << "'status'," - << "'tags'," - << "'entry'," - << "'start'," - << "'due'," - << "'recur'," - << "'end'," - << "'project'," - << "'priority'," - << "'fg'," - << "'bg'," - << "'description'" - << "\n"; - - int count = 0; - - // Get all the tasks. - std::vector tasks; - context.tdb.lock (context.config.getBoolean ("locking")); - handleRecurrence (); - context.tdb.load (tasks, context.filter); - context.tdb.commit (); - context.tdb.unlock (); - - foreach (task, tasks) + if (context.hooks.trigger ("pre-export-command")) { - if (task->getStatus () != Task::recurring) + std::stringstream out; + + // Deliberately no 'id'. + out << "'uuid'," + << "'status'," + << "'tags'," + << "'entry'," + << "'start'," + << "'due'," + << "'recur'," + << "'end'," + << "'project'," + << "'priority'," + << "'fg'," + << "'bg'," + << "'description'" + << "\n"; + + int count = 0; + + // Get all the tasks. + std::vector tasks; + context.tdb.lock (context.config.getBoolean ("locking")); + handleRecurrence (); + context.tdb.load (tasks, context.filter); + context.tdb.commit (); + context.tdb.unlock (); + + foreach (task, tasks) { - out << task->composeCSV ().c_str (); - ++count; + if (task->getStatus () != Task::recurring) + { + out << task->composeCSV ().c_str (); + ++count; + } } + + outs = out.str (); + context.hooks.trigger ("post-export-command"); } - outs = out.str (); - return 0; + return rc; } //////////////////////////////////////////////////////////////////////////////// @@ -1298,291 +1355,316 @@ int handleModify (std::string &outs) //////////////////////////////////////////////////////////////////////////////// int handleAppend (std::string &outs) { - int count = 0; - std::stringstream out; + int rc = 0; - std::vector tasks; - context.tdb.lock (context.config.getBoolean ("locking")); - Filter filter; - context.tdb.loadPending (tasks, filter); - - // Filter sequence. - std::vector all = tasks; - context.filter.applySequence (tasks, context.sequence); - - Permission permission; - if (context.sequence.size () > (size_t) context.config.getInteger ("bulk")) - permission.bigSequence (); - - foreach (task, tasks) + if (context.hooks.trigger ("pre-append-command")) { - foreach (other, all) + int count = 0; + std::stringstream out; + + std::vector tasks; + context.tdb.lock (context.config.getBoolean ("locking")); + Filter filter; + context.tdb.loadPending (tasks, filter); + + // Filter sequence. + std::vector all = tasks; + context.filter.applySequence (tasks, context.sequence); + + Permission permission; + if (context.sequence.size () > (size_t) context.config.getInteger ("bulk")) + permission.bigSequence (); + + foreach (task, tasks) { - if (other->id == task->id || // Self - (task->has ("parent") && - task->get ("parent") == other->get ("parent")) || // Sibling - other->get ("uuid") == task->get ("parent")) // Parent + foreach (other, all) { - Task before (*other); - - // A non-zero value forces a file write. - int changes = 0; - - // Apply other deltas. - changes += deltaAppend (*other); - changes += deltaTags (*other); - changes += deltaAttributes (*other); - - if (taskDiff (before, *other)) + if (other->id == task->id || // Self + (task->has ("parent") && + task->get ("parent") == other->get ("parent")) || // Sibling + other->get ("uuid") == task->get ("parent")) // Parent { - if (changes && permission.confirmed (before, taskDifferences (before, *other) + "Proceed with change?")) + Task before (*other); + + // A non-zero value forces a file write. + int changes = 0; + + // Apply other deltas. + changes += deltaAppend (*other); + changes += deltaTags (*other); + changes += deltaAttributes (*other); + + if (taskDiff (before, *other)) { - context.tdb.update (*other); + if (changes && permission.confirmed (before, taskDifferences (before, *other) + "Proceed with change?")) + { + context.tdb.update (*other); - if (context.config.getBoolean ("echo.command")) - out << "Appended '" - << context.task.get ("description") - << "' to task " - << other->id - << std::endl; + if (context.config.getBoolean ("echo.command")) + out << "Appended '" + << context.task.get ("description") + << "' to task " + << other->id + << std::endl; - ++count; + ++count; + } } } } } + + context.tdb.commit (); + context.tdb.unlock (); + + if (context.config.getBoolean ("echo.command")) + out << "Appended " << count << " task" << (count == 1 ? "" : "s") << std::endl; + + outs = out.str (); + context.hooks.trigger ("post-append-command"); } - context.tdb.commit (); - context.tdb.unlock (); - - if (context.config.getBoolean ("echo.command")) - out << "Appended " << count << " task" << (count == 1 ? "" : "s") << std::endl; - - outs = out.str (); - return 0; + return rc; } //////////////////////////////////////////////////////////////////////////////// int handlePrepend (std::string &outs) { - int count = 0; - std::stringstream out; + int rc = 0; - std::vector tasks; - context.tdb.lock (context.config.getBoolean ("locking")); - Filter filter; - context.tdb.loadPending (tasks, filter); - - // Filter sequence. - std::vector all = tasks; - context.filter.applySequence (tasks, context.sequence); - - Permission permission; - if (context.sequence.size () > (size_t) context.config.getInteger ("bulk")) - permission.bigSequence (); - - foreach (task, tasks) + if (context.hooks.trigger ("pre-prepend-command")) { - foreach (other, all) + int count = 0; + std::stringstream out; + + std::vector tasks; + context.tdb.lock (context.config.getBoolean ("locking")); + Filter filter; + context.tdb.loadPending (tasks, filter); + + // Filter sequence. + std::vector all = tasks; + context.filter.applySequence (tasks, context.sequence); + + Permission permission; + if (context.sequence.size () > (size_t) context.config.getInteger ("bulk")) + permission.bigSequence (); + + foreach (task, tasks) { - if (other->id == task->id || // Self - (task->has ("parent") && - task->get ("parent") == other->get ("parent")) || // Sibling - other->get ("uuid") == task->get ("parent")) // Parent + foreach (other, all) { - Task before (*other); - - // A non-zero value forces a file write. - int changes = 0; - - // Apply other deltas. - changes += deltaPrepend (*other); - changes += deltaTags (*other); - changes += deltaAttributes (*other); - - if (taskDiff (before, *other)) + if (other->id == task->id || // Self + (task->has ("parent") && + task->get ("parent") == other->get ("parent")) || // Sibling + other->get ("uuid") == task->get ("parent")) // Parent { - if (changes && permission.confirmed (before, taskDifferences (before, *other) + "Are you sure?")) + Task before (*other); + + // A non-zero value forces a file write. + int changes = 0; + + // Apply other deltas. + changes += deltaPrepend (*other); + changes += deltaTags (*other); + changes += deltaAttributes (*other); + + if (taskDiff (before, *other)) { - context.tdb.update (*other); + if (changes && permission.confirmed (before, taskDifferences (before, *other) + "Are you sure?")) + { + context.tdb.update (*other); - if (context.config.getBoolean ("echo.command")) - out << "Prepended '" - << context.task.get ("description") - << "' to task " - << other->id - << std::endl; + if (context.config.getBoolean ("echo.command")) + out << "Prepended '" + << context.task.get ("description") + << "' to task " + << other->id + << std::endl; - ++count; + ++count; + } } } } } + + context.tdb.commit (); + context.tdb.unlock (); + + if (context.config.getBoolean ("echo.command")) + out << "Prepended " << count << " task" << (count == 1 ? "" : "s") << std::endl; + + outs = out.str (); + context.hooks.trigger ("post-prepend-command"); } - context.tdb.commit (); - context.tdb.unlock (); - - if (context.config.getBoolean ("echo.command")) - out << "Prepended " << count << " task" << (count == 1 ? "" : "s") << std::endl; - - outs = out.str (); - return 0; + return rc; } //////////////////////////////////////////////////////////////////////////////// int handleDuplicate (std::string &outs) { - std::stringstream out; - int count = 0; + int rc = 0; - std::vector tasks; - context.tdb.lock (context.config.getBoolean ("locking")); - Filter filter; - context.tdb.loadPending (tasks, filter); - - // Filter sequence. - context.filter.applySequence (tasks, context.sequence); - - foreach (task, tasks) + if (context.hooks.trigger ("pre-duplicate-command")) { - Task dup (*task); - dup.set ("uuid", uuid ()); // Needs a new UUID. - dup.setStatus (Task::pending); - dup.remove ("start"); // Does not inherit start date. - dup.remove ("end"); // Does not inherit end date. + std::stringstream out; + int count = 0; - // Recurring tasks are duplicated and downgraded to regular tasks. - if (task->getStatus () == Task::recurring) + std::vector tasks; + context.tdb.lock (context.config.getBoolean ("locking")); + Filter filter; + context.tdb.loadPending (tasks, filter); + + // Filter sequence. + context.filter.applySequence (tasks, context.sequence); + + foreach (task, tasks) { - dup.remove ("parent"); - dup.remove ("recur"); - dup.remove ("until"); - dup.remove ("imak"); - dup.remove ("imask"); + Task dup (*task); + dup.set ("uuid", uuid ()); // Needs a new UUID. + dup.setStatus (Task::pending); + dup.remove ("start"); // Does not inherit start date. + dup.remove ("end"); // Does not inherit end date. - out << "Note: task " - << task->id - << " was a recurring task. The new task is not." - << std::endl; + // Recurring tasks are duplicated and downgraded to regular tasks. + if (task->getStatus () == Task::recurring) + { + dup.remove ("parent"); + dup.remove ("recur"); + dup.remove ("until"); + dup.remove ("imak"); + dup.remove ("imask"); + + out << "Note: task " + << task->id + << " was a recurring task. The new task is not." + << std::endl; + } + + // Apply deltas. + deltaDescription (dup); + deltaTags (dup); + deltaAttributes (dup); + deltaSubstitutions (dup); + + // A New task needs a new entry time. + char entryTime[16]; + sprintf (entryTime, "%u", (unsigned int) time (NULL)); + dup.set ("entry", entryTime); + + context.tdb.add (dup); + + if (context.config.getBoolean ("echo.command")) + out << "Duplicated " + << task->id + << " '" + << task->get ("description") + << "'" + << std::endl; + ++count; } - // Apply deltas. - deltaDescription (dup); - deltaTags (dup); - deltaAttributes (dup); - deltaSubstitutions (dup); - - // A New task needs a new entry time. - char entryTime[16]; - sprintf (entryTime, "%u", (unsigned int) time (NULL)); - dup.set ("entry", entryTime); - - context.tdb.add (dup); - if (context.config.getBoolean ("echo.command")) - out << "Duplicated " - << task->id - << " '" - << task->get ("description") - << "'" - << std::endl; - ++count; - } - - if (context.config.getBoolean ("echo.command")) - { - out << "Duplicated " << count << " task" << (count == 1 ? "" : "s") << std::endl; + { + out << "Duplicated " << count << " task" << (count == 1 ? "" : "s") << std::endl; #ifdef FEATURE_NEW_ID - // All this, just for an id number. - std::vector all; - Filter none; - context.tdb.loadPending (all, none); - out << "Created task " << context.tdb.nextId () << std::endl; + // All this, just for an id number. + std::vector all; + Filter none; + context.tdb.loadPending (all, none); + out << "Created task " << context.tdb.nextId () << std::endl; #endif + } + + context.tdb.commit (); + context.tdb.unlock (); + + outs = out.str (); + context.hooks.trigger ("post-duplicate-command"); } - context.tdb.commit (); - context.tdb.unlock (); - - outs = out.str (); - return 0; + return rc; } //////////////////////////////////////////////////////////////////////////////// #ifdef FEATURE_SHELL void handleShell () { - // Display some kind of welcome message. - Color bold (Color::nocolor, Color::nocolor, false, true, false); - std::cout << ((context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor")) - ? bold.colorize (PACKAGE_STRING) - : PACKAGE_STRING) - << " shell" - << std::endl - << std::endl - << "Enter any task command (such as 'list'), or hit 'Enter'." - << std::endl - << "There is no need to include the 'task' command itself." - << std::endl - << "Enter 'quit' to end the session." - << std::endl - << std::endl; - - // Make a copy because context.clear will delete them. - std::string permanentOverrides = " " + context.file_override - + " " + context.var_overrides; - - std::string quit = "quit"; // TODO i18n - std::string command; - bool keepGoing = true; - - do + if (context.hooks.trigger ("pre-shell-command")) { - std::cout << context.config.get ("shell.prompt") << " "; + // Display some kind of welcome message. + Color bold (Color::nocolor, Color::nocolor, false, true, false); + std::cout << ((context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor")) + ? bold.colorize (PACKAGE_STRING) + : PACKAGE_STRING) + << " shell" + << std::endl + << std::endl + << "Enter any task command (such as 'list'), or hit 'Enter'." + << std::endl + << "There is no need to include the 'task' command itself." + << std::endl + << "Enter 'quit' to end the session." + << std::endl + << std::endl; - command = ""; - std::getline (std::cin, command); - std::string decoratedCommand = trim (command + permanentOverrides); + // Make a copy because context.clear will delete them. + std::string permanentOverrides = " " + context.file_override + + " " + context.var_overrides; - // When looking for the 'quit' command, use 'command', not - // 'decoratedCommand'. - if (command.length () > 0 && - command.length () <= quit.length () && - lowerCase (command) == quit.substr (0, command.length ())) + std::string quit = "quit"; // TODO i18n + std::string command; + bool keepGoing = true; + + do { - keepGoing = false; - } - else - { - try + std::cout << context.config.get ("shell.prompt") << " "; + + command = ""; + std::getline (std::cin, command); + std::string decoratedCommand = trim (command + permanentOverrides); + + // When looking for the 'quit' command, use 'command', not + // 'decoratedCommand'. + if (command.length () > 0 && + command.length () <= quit.length () && + lowerCase (command) == quit.substr (0, command.length ())) { - context.clear (); - - std::vector args; - split (args, decoratedCommand, ' '); - foreach (arg, args) context.args.push_back (*arg); - - context.initialize (); - context.run (); + keepGoing = false; } - - catch (std::string& error) + else { - std::cout << error << std::endl; - } + try + { + context.clear (); - catch (...) - { - std::cerr << context.stringtable.get (100, "Unknown error.") << std::endl; + std::vector args; + split (args, decoratedCommand, ' '); + foreach (arg, args) context.args.push_back (*arg); + + context.initialize (); + context.run (); + } + + catch (std::string& error) + { + std::cout << error << std::endl; + } + + catch (...) + { + std::cerr << context.stringtable.get (100, "Unknown error.") << std::endl; + } } } + while (keepGoing && !std::cin.eof ()); + + // No need to repeat any overrides after the shell quits. + context.clearMessages (); + context.hooks.trigger ("post-shell-command"); } - while (keepGoing && !std::cin.eof ()); - - // No need to repeat any overrides after the shell quits. - context.clearMessages (); } #endif @@ -1590,191 +1672,204 @@ void handleShell () int handleColor (std::string &outs) { int rc = 0; - std::stringstream out; - if (context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor")) + if (context.hooks.trigger ("pre-color-command")) { - // If there is something in the description, then assume that is a color, - // and display it as a sample. - std::string description = context.task.get ("description"); - if (description != "") + std::stringstream out; + + if (context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor")) { - Color one ("black on bright yellow"); - Color two ("underline cyan on bright blue"); - Color three ("color214 on color202"); - Color four ("rgb150 on rgb020"); - Color five ("underline grey10 on grey3"); - Color six ("red on color173"); - Color sample (description); - - out << std::endl - << "Use this command to see how colors are displayed by your terminal." << std::endl - << std::endl - << "16-color usage (supports underline, bold text, bright background):" << std::endl - << " " << one.colorize ("task color black on bright yellow") << std::endl - << " " << two.colorize ("task color underline cyan on bright blue") << std::endl - << std::endl - << "256-color usage (supports underline):" << std::endl - << " " << three.colorize ("task color color214 on color202") << std::endl - << " " << four.colorize ("task color rgb150 on rgb020") << std::endl - << " " << five.colorize ("task color underline grey10 on grey3") << std::endl - << " " << six.colorize ("task color red on color173") << std::endl - << std::endl - << "Your sample:" << std::endl - << " " << sample.colorize ("task color " + description) << std::endl - << std::endl; - } - - // Show all supported colors. Possibly show some unsupported ones too. - else - { - out << std::endl - << "Basic colors" - << std::endl - << " " << Color::colorize (" black ", "black") - << " " << Color::colorize (" red ", "red") - << " " << Color::colorize (" blue ", "blue") - << " " << Color::colorize (" green ", "green") - << " " << Color::colorize (" magenta ", "magenta") - << " " << Color::colorize (" cyan ", "cyan") - << " " << Color::colorize (" yellow ", "yellow") - << " " << Color::colorize (" white ", "white") - << std::endl - << " " << Color::colorize (" black ", "white on black") - << " " << Color::colorize (" red ", "white on red") - << " " << Color::colorize (" blue ", "white on blue") - << " " << Color::colorize (" green ", "black on green") - << " " << Color::colorize (" magenta ", "black on magenta") - << " " << Color::colorize (" cyan ", "black on cyan") - << " " << Color::colorize (" yellow ", "black on yellow") - << " " << Color::colorize (" white ", "black on white") - << std::endl - << std::endl; - - out << "Effects" - << std::endl - << " " << Color::colorize (" red ", "red") - << " " << Color::colorize (" bold red ", "bold red") - << " " << Color::colorize (" underline on blue ", "underline on blue") - << " " << Color::colorize (" on green ", "black on green") - << " " << Color::colorize (" on bright green ", "black on bright green") - << std::endl - << std::endl; - - // 16 system colors. - out << "color0 - color15" << std::endl; - for (int r = 0; r < 2; ++r) + // If there is something in the description, then assume that is a color, + // and display it as a sample. + std::string description = context.task.get ("description"); + if (description != "") { - out << " "; - for (int c = 0; c < 8; ++c) - { - std::stringstream s; - s << "on color" << (r*8 + c); - out << Color::colorize (" ", s.str ()); - } + Color one ("black on bright yellow"); + Color two ("underline cyan on bright blue"); + Color three ("color214 on color202"); + Color four ("rgb150 on rgb020"); + Color five ("underline grey10 on grey3"); + Color six ("red on color173"); + Color sample (description); - out << std::endl; + out << std::endl + << "Use this command to see how colors are displayed by your terminal." << std::endl + << std::endl + << "16-color usage (supports underline, bold text, bright background):" << std::endl + << " " << one.colorize ("task color black on bright yellow") << std::endl + << " " << two.colorize ("task color underline cyan on bright blue") << std::endl + << std::endl + << "256-color usage (supports underline):" << std::endl + << " " << three.colorize ("task color color214 on color202") << std::endl + << " " << four.colorize ("task color rgb150 on rgb020") << std::endl + << " " << five.colorize ("task color underline grey10 on grey3") << std::endl + << " " << six.colorize ("task color red on color173") << std::endl + << std::endl + << "Your sample:" << std::endl + << " " << sample.colorize ("task color " + description) << std::endl + << std::endl; } - out << std::endl; - - // Color cube. - out << "Color cube rgb000 - rgb555 (also color16 - color231)" << std::endl; - for (int g = 0; g < 6; ++g) + // Show all supported colors. Possibly show some unsupported ones too. + else { - out << " "; - for (int r = 0; r < 6; ++r) + out << std::endl + << "Basic colors" + << std::endl + << " " << Color::colorize (" black ", "black") + << " " << Color::colorize (" red ", "red") + << " " << Color::colorize (" blue ", "blue") + << " " << Color::colorize (" green ", "green") + << " " << Color::colorize (" magenta ", "magenta") + << " " << Color::colorize (" cyan ", "cyan") + << " " << Color::colorize (" yellow ", "yellow") + << " " << Color::colorize (" white ", "white") + << std::endl + << " " << Color::colorize (" black ", "white on black") + << " " << Color::colorize (" red ", "white on red") + << " " << Color::colorize (" blue ", "white on blue") + << " " << Color::colorize (" green ", "black on green") + << " " << Color::colorize (" magenta ", "black on magenta") + << " " << Color::colorize (" cyan ", "black on cyan") + << " " << Color::colorize (" yellow ", "black on yellow") + << " " << Color::colorize (" white ", "black on white") + << std::endl + << std::endl; + + out << "Effects" + << std::endl + << " " << Color::colorize (" red ", "red") + << " " << Color::colorize (" bold red ", "bold red") + << " " << Color::colorize (" underline on blue ", "underline on blue") + << " " << Color::colorize (" on green ", "black on green") + << " " << Color::colorize (" on bright green ", "black on bright green") + << std::endl + << std::endl; + + // 16 system colors. + out << "color0 - color15" << std::endl; + for (int r = 0; r < 2; ++r) { - for (int b = 0; b < 6; ++b) + out << " "; + for (int c = 0; c < 8; ++c) { std::stringstream s; - s << "on rgb" << r << g << b; + s << "on color" << (r*8 + c); out << Color::colorize (" ", s.str ()); } - out << " "; + out << std::endl; } out << std::endl; + + // Color cube. + out << "Color cube rgb000 - rgb555 (also color16 - color231)" << std::endl; + for (int g = 0; g < 6; ++g) + { + out << " "; + for (int r = 0; r < 6; ++r) + { + for (int b = 0; b < 6; ++b) + { + std::stringstream s; + s << "on rgb" << r << g << b; + out << Color::colorize (" ", s.str ()); + } + + out << " "; + } + + out << std::endl; + } + + out << std::endl; + + // Grey ramp. + out << "Gray ramp gray0 - gray23 (also color232 - color255)" << std::endl << " "; + for (int g = 0; g < 24; ++g) + { + std::stringstream s; + s << "on gray" << g; + out << Color::colorize (" ", s.str ()); + } + + out << std::endl + << std::endl + << "Try running 'task color white on red'" + << std::endl + << std::endl; } - - out << std::endl; - - // Grey ramp. - out << "Gray ramp gray0 - gray23 (also color232 - color255)" << std::endl << " "; - for (int g = 0; g < 24; ++g) - { - std::stringstream s; - s << "on gray" << g; - out << Color::colorize (" ", s.str ()); - } - - out << std::endl - << std::endl - << "Try running 'task color white on red'" - << std::endl - << std::endl; } - } - else - { - out << "Color is currently turned off in your .taskrc file. To enable " - "color, remove the line 'color=off', or change the 'off' to 'on'." - << std::endl; - rc = 1; + else + { + out << "Color is currently turned off in your .taskrc file. To enable " + "color, remove the line 'color=off', or change the 'off' to 'on'." + << std::endl; + rc = 1; + } + + outs = out.str (); + context.hooks.trigger ("post-color-command"); } - outs = out.str (); return rc; } //////////////////////////////////////////////////////////////////////////////// int handleAnnotate (std::string &outs) { - if (!context.task.has ("description")) - throw std::string ("Cannot apply a blank annotation."); + int rc = 0; - std::stringstream out; - - std::vector tasks; - context.tdb.lock (context.config.getBoolean ("locking")); - Filter filter; - context.tdb.loadPending (tasks, filter); - - // Filter sequence. - context.filter.applySequence (tasks, context.sequence); - - Permission permission; - if (context.sequence.size () > (size_t) context.config.getInteger ("bulk")) - permission.bigSequence (); - - foreach (task, tasks) + if (context.hooks.trigger ("pre-annotate-command")) { - Task before (*task); - task->addAnnotation (context.task.get ("description")); + if (!context.task.has ("description")) + throw std::string ("Cannot apply a blank annotation."); - if (taskDiff (before, *task)) + std::stringstream out; + + std::vector tasks; + context.tdb.lock (context.config.getBoolean ("locking")); + Filter filter; + context.tdb.loadPending (tasks, filter); + + // Filter sequence. + context.filter.applySequence (tasks, context.sequence); + + Permission permission; + if (context.sequence.size () > (size_t) context.config.getInteger ("bulk")) + permission.bigSequence (); + + foreach (task, tasks) { - if (permission.confirmed (before, taskDifferences (before, *task) + "Proceed with change?")) - { - context.tdb.update (*task); + Task before (*task); + task->addAnnotation (context.task.get ("description")); - if (context.config.getBoolean ("echo.command")) - out << "Annotated " - << task->id - << " with '" - << context.task.get ("description") - << "'" - << std::endl; + if (taskDiff (before, *task)) + { + if (permission.confirmed (before, taskDifferences (before, *task) + "Proceed with change?")) + { + context.tdb.update (*task); + + if (context.config.getBoolean ("echo.command")) + out << "Annotated " + << task->id + << " with '" + << context.task.get ("description") + << "'" + << std::endl; + } } } + + context.tdb.commit (); + context.tdb.unlock (); + + outs = out.str (); + context.hooks.trigger ("post-annotate-command"); } - context.tdb.commit (); - context.tdb.unlock (); - - outs = out.str (); - return 0; + return rc; } //////////////////////////////////////////////////////////////////////////////// diff --git a/src/custom.cpp b/src/custom.cpp index 7614c0c93..0a7533faa 100644 --- a/src/custom.cpp +++ b/src/custom.cpp @@ -111,522 +111,530 @@ int runCustomReport ( std::string &outs) { int rc = 0; - // Load report configuration. - std::vector columns; - split (columns, columnList, ','); - validReportColumns (columns); - std::vector labels; - split (labels, labelList, ','); - - if (columns.size () != labels.size () && labels.size () != 0) - throw std::string ("There are a different number of columns than labels ") + - "for report '" + report + "'."; - - std::map columnLabels; - if (labels.size ()) - for (unsigned int i = 0; i < columns.size (); ++i) - columnLabels[columns[i]] = labels[i]; - - std::vector sortOrder; - split (sortOrder, sortList, ','); - validSortColumns (columns, sortOrder); - - std::vector filterArgs; - split (filterArgs, filterList, ' '); + if (context.hooks.trigger ("pre-custom-report-command") && + context.hooks.trigger (std::string ("pre-") + report + "-command")) { - Cmd cmd (report); - Task task; - Sequence sequence; - Subst subst; - Filter filter; - context.parse (filterArgs, cmd, task, sequence, subst, filter); + // Load report configuration. + std::vector columns; + split (columns, columnList, ','); + validReportColumns (columns); - context.sequence.combine (sequence); + std::vector labels; + split (labels, labelList, ','); - // Allow limit to be overridden by the command line. - if (!context.task.has ("limit") && task.has ("limit")) - context.task.set ("limit", task.get ("limit")); + if (columns.size () != labels.size () && labels.size () != 0) + throw std::string ("There are a different number of columns than labels ") + + "for report '" + report + "'."; - foreach (att, filter) - context.filter.push_back (*att); - } + std::map columnLabels; + if (labels.size ()) + for (unsigned int i = 0; i < columns.size (); ++i) + columnLabels[columns[i]] = labels[i]; - // Filter sequence. - if (context.sequence.size ()) - context.filter.applySequence (tasks, context.sequence); + std::vector sortOrder; + split (sortOrder, sortList, ','); + validSortColumns (columns, sortOrder); - // Initialize colorization for subsequent auto colorization. - initializeColorRules (); - - Table table; - table.setTableWidth (context.getWidth ()); - table.setDateFormat (context.config.get ("dateformat")); - table.setReportName (report); - - foreach (task, tasks) - table.addRow (); - - int columnCount = 0; - int dueColumn = -1; - foreach (col, columns) - { - // Add each column individually. - if (*col == "id") + std::vector filterArgs; + split (filterArgs, filterList, ' '); { - table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "ID"); - table.setColumnWidth (columnCount, Table::minimum); - table.setColumnJustification (columnCount, Table::right); + Cmd cmd (report); + Task task; + Sequence sequence; + Subst subst; + Filter filter; + context.parse (filterArgs, cmd, task, sequence, subst, filter); - int row = 0; - foreach (task, tasks) - if (task->id != 0) - table.addCell (row++, columnCount, task->id); - else - table.addCell (row++, columnCount, "-"); + context.sequence.combine (sequence); + + // Allow limit to be overridden by the command line. + if (!context.task.has ("limit") && task.has ("limit")) + context.task.set ("limit", task.get ("limit")); + + foreach (att, filter) + context.filter.push_back (*att); } - else if (*col == "uuid") + // Filter sequence. + if (context.sequence.size ()) + context.filter.applySequence (tasks, context.sequence); + + // Initialize colorization for subsequent auto colorization. + initializeColorRules (); + + Table table; + table.setTableWidth (context.getWidth ()); + table.setDateFormat (context.config.get ("dateformat")); + table.setReportName (report); + + foreach (task, tasks) + table.addRow (); + + int columnCount = 0; + int dueColumn = -1; + foreach (col, columns) { - table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "UUID"); - table.setColumnWidth (columnCount, Table::minimum); - table.setColumnJustification (columnCount, Table::left); - - int row = 0; - foreach (task, tasks) - table.addCell (row++, columnCount, task->get ("uuid")); - } - - else if (*col == "project") - { - table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "Project"); - table.setColumnWidth (columnCount, Table::minimum); - table.setColumnJustification (columnCount, Table::left); - - int row = 0; - foreach (task, tasks) - table.addCell (row++, columnCount, task->get ("project")); - } - - else if (*col == "priority") - { - table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "Pri"); - table.setColumnWidth (columnCount, Table::minimum); - table.setColumnJustification (columnCount, Table::left); - - int row = 0; - foreach (task, tasks) - table.addCell (row++, columnCount, task->get ("priority")); - } - - else if (*col == "priority_long") - { - table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "Pri"); - table.setColumnWidth (columnCount, Table::minimum); - table.setColumnJustification (columnCount, Table::left); - - int row = 0; - std::string pri; - foreach (task, tasks) + // Add each column individually. + if (*col == "id") { - pri = task->get ("priority"); + table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "ID"); + table.setColumnWidth (columnCount, Table::minimum); + table.setColumnJustification (columnCount, Table::right); - if (pri == "H") pri = "High"; // TODO i18n - else if (pri == "M") pri = "Medium"; // TODO i18n - else if (pri == "L") pri = "Low"; // TODO i18n - - table.addCell (row++, columnCount, pri); + int row = 0; + foreach (task, tasks) + if (task->id != 0) + table.addCell (row++, columnCount, task->id); + else + table.addCell (row++, columnCount, "-"); } - } - else if (*col == "entry") - { - table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "Added"); - table.setColumnWidth (columnCount, Table::minimum); - table.setColumnJustification (columnCount, Table::right); - - std::string entered; - for (unsigned int row = 0; row < tasks.size(); ++row) + else if (*col == "uuid") { - entered = tasks[row].get ("entry"); - if (entered.length ()) + table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "UUID"); + table.setColumnWidth (columnCount, Table::minimum); + table.setColumnJustification (columnCount, Table::left); + + int row = 0; + foreach (task, tasks) + table.addCell (row++, columnCount, task->get ("uuid")); + } + + else if (*col == "project") + { + table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "Project"); + table.setColumnWidth (columnCount, Table::minimum); + table.setColumnJustification (columnCount, Table::left); + + int row = 0; + foreach (task, tasks) + table.addCell (row++, columnCount, task->get ("project")); + } + + else if (*col == "priority") + { + table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "Pri"); + table.setColumnWidth (columnCount, Table::minimum); + table.setColumnJustification (columnCount, Table::left); + + int row = 0; + foreach (task, tasks) + table.addCell (row++, columnCount, task->get ("priority")); + } + + else if (*col == "priority_long") + { + table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "Pri"); + table.setColumnWidth (columnCount, Table::minimum); + table.setColumnJustification (columnCount, Table::left); + + int row = 0; + std::string pri; + foreach (task, tasks) { - Date dt (::atoi (entered.c_str ())); - entered = dt.toString (context.config.get ("dateformat")); - table.addCell (row, columnCount, entered); + pri = task->get ("priority"); + + if (pri == "H") pri = "High"; // TODO i18n + else if (pri == "M") pri = "Medium"; // TODO i18n + else if (pri == "L") pri = "Low"; // TODO i18n + + table.addCell (row++, columnCount, pri); } } - } - else if (*col == "entry_time") - { - table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "Added"); - table.setColumnWidth (columnCount, Table::minimum); - table.setColumnJustification (columnCount, Table::right); - - std::string entered; - for (unsigned int row = 0; row < tasks.size(); ++row) + else if (*col == "entry") { - entered = tasks[row].get ("entry"); - if (entered.length ()) + table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "Added"); + table.setColumnWidth (columnCount, Table::minimum); + table.setColumnJustification (columnCount, Table::right); + + std::string entered; + for (unsigned int row = 0; row < tasks.size(); ++row) { - Date dt (::atoi (entered.c_str ())); - entered = dt.toStringWithTime (context.config.get ("dateformat")); - table.addCell (row, columnCount, entered); + entered = tasks[row].get ("entry"); + if (entered.length ()) + { + Date dt (::atoi (entered.c_str ())); + entered = dt.toString (context.config.get ("dateformat")); + table.addCell (row, columnCount, entered); + } } } - } - else if (*col == "start") - { - table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "Started"); - table.setColumnWidth (columnCount, Table::minimum); - table.setColumnJustification (columnCount, Table::right); - - std::string started; - for (unsigned int row = 0; row < tasks.size(); ++row) + else if (*col == "entry_time") { - started = tasks[row].get ("start"); - if (started.length ()) + table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "Added"); + table.setColumnWidth (columnCount, Table::minimum); + table.setColumnJustification (columnCount, Table::right); + + std::string entered; + for (unsigned int row = 0; row < tasks.size(); ++row) { - Date dt (::atoi (started.c_str ())); - started = dt.toString (context.config.get ("dateformat")); - table.addCell (row, columnCount, started); + entered = tasks[row].get ("entry"); + if (entered.length ()) + { + Date dt (::atoi (entered.c_str ())); + entered = dt.toStringWithTime (context.config.get ("dateformat")); + table.addCell (row, columnCount, entered); + } } } - } - else if (*col == "start_time") - { - table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "Started"); - table.setColumnWidth (columnCount, Table::minimum); - table.setColumnJustification (columnCount, Table::right); - - std::string started; - for (unsigned int row = 0; row < tasks.size(); ++row) + else if (*col == "start") { - started = tasks[row].get ("start"); - if (started.length ()) + table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "Started"); + table.setColumnWidth (columnCount, Table::minimum); + table.setColumnJustification (columnCount, Table::right); + + std::string started; + for (unsigned int row = 0; row < tasks.size(); ++row) { - Date dt (::atoi (started.c_str ())); - started = dt.toStringWithTime (context.config.get ("dateformat")); - table.addCell (row, columnCount, started); + started = tasks[row].get ("start"); + if (started.length ()) + { + Date dt (::atoi (started.c_str ())); + started = dt.toString (context.config.get ("dateformat")); + table.addCell (row, columnCount, started); + } } } - } - else if (*col == "end") - { - table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "Completed"); - table.setColumnWidth (columnCount, Table::minimum); - table.setColumnJustification (columnCount, Table::right); - - std::string started; - for (unsigned int row = 0; row < tasks.size(); ++row) + else if (*col == "start_time") { - started = tasks[row].get ("end"); - if (started.length ()) + table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "Started"); + table.setColumnWidth (columnCount, Table::minimum); + table.setColumnJustification (columnCount, Table::right); + + std::string started; + for (unsigned int row = 0; row < tasks.size(); ++row) { - Date dt (::atoi (started.c_str ())); - started = dt.toString (context.config.get ("dateformat")); - table.addCell (row, columnCount, started); + started = tasks[row].get ("start"); + if (started.length ()) + { + Date dt (::atoi (started.c_str ())); + started = dt.toStringWithTime (context.config.get ("dateformat")); + table.addCell (row, columnCount, started); + } } } - } - else if (*col == "end_time") - { - table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "Completed"); - table.setColumnWidth (columnCount, Table::minimum); - table.setColumnJustification (columnCount, Table::right); - - std::string format = context.config.get ("dateformat"); - - std::string started; - for (unsigned int row = 0; row < tasks.size(); ++row) + else if (*col == "end") { - started = tasks[row].get ("end"); - if (started.length ()) + table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "Completed"); + table.setColumnWidth (columnCount, Table::minimum); + table.setColumnJustification (columnCount, Table::right); + + std::string started; + for (unsigned int row = 0; row < tasks.size(); ++row) { - Date dt (::atoi (started.c_str ())); - started = dt.toStringWithTime (format); - table.addCell (row, columnCount, started); + started = tasks[row].get ("end"); + if (started.length ()) + { + Date dt (::atoi (started.c_str ())); + started = dt.toString (context.config.get ("dateformat")); + table.addCell (row, columnCount, started); + } } } - } - else if (*col == "due") - { - table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "Due"); - table.setColumnWidth (columnCount, Table::minimum); - table.setColumnJustification (columnCount, Table::left); - - std::string format = context.config.get ("report." + report + ".dateformat"); - if (format == "") - format = context.config.get ("dateformat.report"); - if (format == "") - format = context.config.get ("dateformat"); - - int row = 0; - std::string due; - foreach (task, tasks) - table.addCell (row++, columnCount, getDueDate (*task, format)); - - dueColumn = columnCount; - } - - else if (*col == "age") - { - table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "Age"); - table.setColumnWidth (columnCount, Table::minimum); - table.setColumnJustification (columnCount, Table::right); - - std::string created; - std::string age; - Date now; - for (unsigned int row = 0; row < tasks.size(); ++row) + else if (*col == "end_time") { - created = tasks[row].get ("entry"); - if (created.length ()) + table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "Completed"); + table.setColumnWidth (columnCount, Table::minimum); + table.setColumnJustification (columnCount, Table::right); + + std::string format = context.config.get ("dateformat"); + + std::string started; + for (unsigned int row = 0; row < tasks.size(); ++row) { - Date dt (::atoi (created.c_str ())); - age = formatSeconds ((time_t) (now - dt)); - table.addCell (row, columnCount, age); + started = tasks[row].get ("end"); + if (started.length ()) + { + Date dt (::atoi (started.c_str ())); + started = dt.toStringWithTime (format); + table.addCell (row, columnCount, started); + } } } - } - else if (*col == "age_compact") - { - table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "Age"); - table.setColumnWidth (columnCount, Table::minimum); - table.setColumnJustification (columnCount, Table::right); - - std::string created; - std::string age; - Date now; - for (unsigned int row = 0; row < tasks.size(); ++row) + else if (*col == "due") { - created = tasks[row].get ("entry"); - if (created.length ()) + table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "Due"); + table.setColumnWidth (columnCount, Table::minimum); + table.setColumnJustification (columnCount, Table::left); + + std::string format = context.config.get ("report." + report + ".dateformat"); + if (format == "") + format = context.config.get ("dateformat.report"); + if (format == "") + format = context.config.get ("dateformat"); + + int row = 0; + std::string due; + foreach (task, tasks) + table.addCell (row++, columnCount, getDueDate (*task, format)); + + dueColumn = columnCount; + } + + else if (*col == "age") + { + table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "Age"); + table.setColumnWidth (columnCount, Table::minimum); + table.setColumnJustification (columnCount, Table::right); + + std::string created; + std::string age; + Date now; + for (unsigned int row = 0; row < tasks.size(); ++row) { - Date dt (::atoi (created.c_str ())); - age = formatSecondsCompact ((time_t) (now - dt)); - table.addCell (row, columnCount, age); + created = tasks[row].get ("entry"); + if (created.length ()) + { + Date dt (::atoi (created.c_str ())); + age = formatSeconds ((time_t) (now - dt)); + table.addCell (row, columnCount, age); + } } } - } - else if (*col == "active") - { - table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "Active"); - table.setColumnWidth (columnCount, Table::minimum); - table.setColumnJustification (columnCount, Table::left); - - for (unsigned int row = 0; row < tasks.size(); ++row) - if (tasks[row].has ("start")) - table.addCell (row, columnCount, "*"); - } - - else if (*col == "tags") - { - table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "Tags"); - table.setColumnWidth (columnCount, Table::minimum); - table.setColumnJustification (columnCount, Table::left); - - int row = 0; - std::vector all; - std::string tags; - foreach (task, tasks) + else if (*col == "age_compact") { - task->getTags (all); - join (tags, " ", all); - table.addCell (row++, columnCount, tags); - } - } + table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "Age"); + table.setColumnWidth (columnCount, Table::minimum); + table.setColumnJustification (columnCount, Table::right); - else if (*col == "description_only") - { - table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "Description"); - table.setColumnWidth (columnCount, Table::flexible); - table.setColumnJustification (columnCount, Table::left); - - int row = 0; - foreach (task, tasks) - table.addCell (row++, columnCount, task->get ("description")); - } - - else if (*col == "description") - { - table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "Description"); - table.setColumnWidth (columnCount, Table::flexible); - table.setColumnJustification (columnCount, Table::left); - - int row = 0; - foreach (task, tasks) - table.addCell (row++, columnCount, getFullDescription (*task, report)); - } - - else if (*col == "recur") - { - table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "Recur"); - table.setColumnWidth (columnCount, Table::minimum); - table.setColumnJustification (columnCount, Table::right); - - for (unsigned int row = 0; row < tasks.size(); ++row) - { - std::string recur = tasks[row].get ("recur"); - if (recur != "") - table.addCell (row, columnCount, recur); - } - } - - else if (*col == "recurrence_indicator") - { - table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "R"); - table.setColumnWidth (columnCount, Table::minimum); - table.setColumnJustification (columnCount, Table::right); - - for (unsigned int row = 0; row < tasks.size(); ++row) - if (tasks[row].has ("recur")) - table.addCell (row, columnCount, "R"); - } - - else if (*col == "tag_indicator") - { - table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "T"); - table.setColumnWidth (columnCount, Table::minimum); - table.setColumnJustification (columnCount, Table::right); - - for (unsigned int row = 0; row < tasks.size(); ++row) - if (tasks[row].getTagCount ()) - table.addCell (row, columnCount, "+"); - } - - else if (*col == "wait") - { - table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "Wait"); - table.setColumnWidth (columnCount, Table::minimum); - table.setColumnJustification (columnCount, Table::right); - - int row = 0; - std::string wait; - foreach (task, tasks) - { - wait = task->get ("wait"); - if (wait != "") + std::string created; + std::string age; + Date now; + for (unsigned int row = 0; row < tasks.size(); ++row) { - Date dt (::atoi (wait.c_str ())); - wait = dt.toString (context.config.get ("dateformat")); - table.addCell (row++, columnCount, wait); + created = tasks[row].get ("entry"); + if (created.length ()) + { + Date dt (::atoi (created.c_str ())); + age = formatSecondsCompact ((time_t) (now - dt)); + table.addCell (row, columnCount, age); + } } } + + else if (*col == "active") + { + table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "Active"); + table.setColumnWidth (columnCount, Table::minimum); + table.setColumnJustification (columnCount, Table::left); + + for (unsigned int row = 0; row < tasks.size(); ++row) + if (tasks[row].has ("start")) + table.addCell (row, columnCount, "*"); + } + + else if (*col == "tags") + { + table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "Tags"); + table.setColumnWidth (columnCount, Table::minimum); + table.setColumnJustification (columnCount, Table::left); + + int row = 0; + std::vector all; + std::string tags; + foreach (task, tasks) + { + task->getTags (all); + join (tags, " ", all); + table.addCell (row++, columnCount, tags); + } + } + + else if (*col == "description_only") + { + table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "Description"); + table.setColumnWidth (columnCount, Table::flexible); + table.setColumnJustification (columnCount, Table::left); + + int row = 0; + foreach (task, tasks) + table.addCell (row++, columnCount, task->get ("description")); + } + + else if (*col == "description") + { + table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "Description"); + table.setColumnWidth (columnCount, Table::flexible); + table.setColumnJustification (columnCount, Table::left); + + int row = 0; + foreach (task, tasks) + table.addCell (row++, columnCount, getFullDescription (*task, report)); + } + + else if (*col == "recur") + { + table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "Recur"); + table.setColumnWidth (columnCount, Table::minimum); + table.setColumnJustification (columnCount, Table::right); + + for (unsigned int row = 0; row < tasks.size(); ++row) + { + std::string recur = tasks[row].get ("recur"); + if (recur != "") + table.addCell (row, columnCount, recur); + } + } + + else if (*col == "recurrence_indicator") + { + table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "R"); + table.setColumnWidth (columnCount, Table::minimum); + table.setColumnJustification (columnCount, Table::right); + + for (unsigned int row = 0; row < tasks.size(); ++row) + if (tasks[row].has ("recur")) + table.addCell (row, columnCount, "R"); + } + + else if (*col == "tag_indicator") + { + table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "T"); + table.setColumnWidth (columnCount, Table::minimum); + table.setColumnJustification (columnCount, Table::right); + + for (unsigned int row = 0; row < tasks.size(); ++row) + if (tasks[row].getTagCount ()) + table.addCell (row, columnCount, "+"); + } + + else if (*col == "wait") + { + table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "Wait"); + table.setColumnWidth (columnCount, Table::minimum); + table.setColumnJustification (columnCount, Table::right); + + int row = 0; + std::string wait; + foreach (task, tasks) + { + wait = task->get ("wait"); + if (wait != "") + { + Date dt (::atoi (wait.c_str ())); + wait = dt.toString (context.config.get ("dateformat")); + table.addCell (row++, columnCount, wait); + } + } + } + + // Common to all columns. + // Add underline. + if ((context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor")) && + context.config.getBoolean ("fontunderline")) + table.setColumnUnderline (columnCount); + else + table.setTableDashedUnderline (); + + ++columnCount; } - // Common to all columns. - // Add underline. - if ((context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor")) && - context.config.getBoolean ("fontunderline")) - table.setColumnUnderline (columnCount); - else - table.setTableDashedUnderline (); + // Dynamically add sort criteria. + // Build a map of column names -> index. + std::map columnIndex; + for (unsigned int c = 0; c < columns.size (); ++c) + columnIndex[columns[c]] = c; - ++columnCount; - } - - // Dynamically add sort criteria. - // Build a map of column names -> index. - std::map columnIndex; - for (unsigned int c = 0; c < columns.size (); ++c) - columnIndex[columns[c]] = c; - - foreach (sortColumn, sortOrder) - { - // Separate column and direction. - std::string column = sortColumn->substr (0, sortColumn->length () - 1); - char direction = (*sortColumn)[sortColumn->length () - 1]; - - // TODO This code should really be using Att::type. - if (column == "id") - table.sortOn (columnIndex[column], - (direction == '+' ? - Table::ascendingNumeric : - Table::descendingNumeric)); - - else if (column == "priority") - table.sortOn (columnIndex[column], - (direction == '+' ? - Table::ascendingPriority : - Table::descendingPriority)); - - else if (column == "entry" || column == "start" || column == "wait" || - column == "until" || column == "end") - table.sortOn (columnIndex[column], - (direction == '+' ? - Table::ascendingDate : - Table::descendingDate)); - - else if (column == "due") - table.sortOn (columnIndex[column], - (direction == '+' ? - Table::ascendingDueDate : - Table::descendingDueDate)); - - else if (column == "recur") - table.sortOn (columnIndex[column], - (direction == '+' ? - Table::ascendingPeriod : - Table::descendingPeriod)); - - else - table.sortOn (columnIndex[column], - (direction == '+' ? - Table::ascendingCharacter : - Table::descendingCharacter)); - } - - // Now auto colorize all rows. - if (context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor")) - { - for (unsigned int row = 0; row < tasks.size (); ++row) + foreach (sortColumn, sortOrder) { - Color c (tasks[row].get ("fg") + " " + tasks[row].get ("bg")); - autoColorize (tasks[row], c); - table.setRowColor (row, c); + // Separate column and direction. + std::string column = sortColumn->substr (0, sortColumn->length () - 1); + char direction = (*sortColumn)[sortColumn->length () - 1]; + + // TODO This code should really be using Att::type. + if (column == "id") + table.sortOn (columnIndex[column], + (direction == '+' ? + Table::ascendingNumeric : + Table::descendingNumeric)); + + else if (column == "priority") + table.sortOn (columnIndex[column], + (direction == '+' ? + Table::ascendingPriority : + Table::descendingPriority)); + + else if (column == "entry" || column == "start" || column == "wait" || + column == "until" || column == "end") + table.sortOn (columnIndex[column], + (direction == '+' ? + Table::ascendingDate : + Table::descendingDate)); + + else if (column == "due") + table.sortOn (columnIndex[column], + (direction == '+' ? + Table::ascendingDueDate : + Table::descendingDueDate)); + + else if (column == "recur") + table.sortOn (columnIndex[column], + (direction == '+' ? + Table::ascendingPeriod : + Table::descendingPeriod)); + + else + table.sortOn (columnIndex[column], + (direction == '+' ? + Table::ascendingCharacter : + Table::descendingCharacter)); } - } - // If an alternating row color is specified, notify the table. - if (context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor")) - { - Color alternate (context.config.get ("color.alternate")); - if (alternate.nontrivial ()) - table.setTableAlternateColor (alternate); - } + // Now auto colorize all rows. + if (context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor")) + { + for (unsigned int row = 0; row < tasks.size (); ++row) + { + Color c (tasks[row].get ("fg") + " " + tasks[row].get ("bg")); + autoColorize (tasks[row], c); + table.setRowColor (row, c); + } + } - // Limit the number of rows according to the report definition. - int maximum = context.config.getInteger (std::string ("report.") + report + ".limit"); + // If an alternating row color is specified, notify the table. + if (context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor")) + { + Color alternate (context.config.get ("color.alternate")); + if (alternate.nontrivial ()) + table.setTableAlternateColor (alternate); + } - // If the custom report has a defined limit, then allow a numeric override. - // This is an integer specified as a filter (limit:10). - if (context.task.has ("limit")) - maximum = atoi (context.task.get ("limit").c_str ()); + // Limit the number of rows according to the report definition. + int maximum = context.config.getInteger (std::string ("report.") + report + ".limit"); - std::stringstream out; - if (table.rowCount ()) - out << optionalBlankLine () - << table.render (maximum) - << optionalBlankLine () - << table.rowCount () - << (table.rowCount () == 1 ? " task" : " tasks") + // If the custom report has a defined limit, then allow a numeric override. + // This is an integer specified as a filter (limit:10). + if (context.task.has ("limit")) + maximum = atoi (context.task.get ("limit").c_str ()); + + std::stringstream out; + if (table.rowCount ()) + out << optionalBlankLine () + << table.render (maximum) + << optionalBlankLine () + << table.rowCount () + << (table.rowCount () == 1 ? " task" : " tasks") + << std::endl; + else { + out << "No matches." << std::endl; - else { - out << "No matches." - << std::endl; - rc = 1; + rc = 1; + } + + outs = out.str (); + context.hooks.trigger (std::string ("post-") + report + "-command"); + context.hooks.trigger ("post-custom-report-command"); } - outs = out.str (); return rc; } diff --git a/src/edit.cpp b/src/edit.cpp index 14ec35bd0..b094193e4 100644 --- a/src/edit.cpp +++ b/src/edit.cpp @@ -603,50 +603,57 @@ ARE_THESE_REALLY_HARMFUL: // wrench. To be used sparingly. int handleEdit (std::string &outs) { - std::stringstream out; + int rc = 0; - std::vector tasks; - context.tdb.lock (context.config.getBoolean ("locking")); - handleRecurrence (); - Filter filter; - context.tdb.loadPending (tasks, filter); - - // Filter sequence. - std::vector all = tasks; - context.filter.applySequence (tasks, context.sequence); - - foreach (task, tasks) + if (context.hooks.trigger ("pre-edit-command")) { - editFile (*task); - context.tdb.update (*task); -/* - foreach (other, all) + std::stringstream out; + + std::vector tasks; + context.tdb.lock (context.config.getBoolean ("locking")); + handleRecurrence (); + Filter filter; + context.tdb.loadPending (tasks, filter); + + // Filter sequence. + std::vector all = tasks; + context.filter.applySequence (tasks, context.sequence); + + foreach (task, tasks) { - if (other->id != task.id) // Don't edit the same task again. + editFile (*task); + context.tdb.update (*task); +/* + foreach (other, all) { - if (task.has ("parent") && - if (other is parent of task) + if (other->id != task.id) // Don't edit the same task again. { - // Transfer everything but mask, imask, uuid, parent. - } - else if (task is parent of other) - { - // Transfer everything but mask, imask, uuid, parent. - } - else if (task and other are siblings) - { - // Transfer everything but mask, imask, uuid, parent. + if (task.has ("parent") && + if (other is parent of task) + { + // Transfer everything but mask, imask, uuid, parent. + } + else if (task is parent of other) + { + // Transfer everything but mask, imask, uuid, parent. + } + else if (task and other are siblings) + { + // Transfer everything but mask, imask, uuid, parent. + } } } - } */ + } + + context.tdb.commit (); + context.tdb.unlock (); + + outs = out.str (); + context.hooks.trigger ("post-edit-command"); } - context.tdb.commit (); - context.tdb.unlock (); - - outs = out.str (); - return 0; + return rc; } //////////////////////////////////////////////////////////////////////////////// diff --git a/src/import.cpp b/src/import.cpp index c6389ae63..4c453eefc 100644 --- a/src/import.cpp +++ b/src/import.cpp @@ -1148,74 +1148,81 @@ static std::string importCSV (const std::vector & lines) //////////////////////////////////////////////////////////////////////////////// int handleImport (std::string &outs) { - std::stringstream out; + int rc = 0; - // Use the description as a file name. - std::string file = trim (context.task.get ("description")); - if (file.length () > 0) + if (context.hooks.trigger ("pre-import-command")) { - // Load the file. - std::vector all; - File::read (file, all); + std::stringstream out; - std::vector lines; - std::vector ::iterator it; - for (it = all.begin (); it != all.end (); ++it) + // Use the description as a file name. + std::string file = trim (context.task.get ("description")); + if (file.length () > 0) { - std::string line = *it; + // Load the file. + std::vector all; + File::read (file, all); - // Strip comments - std::string::size_type pound = line.find ("#"); - if (pound != std::string::npos) - line = line.substr (0, pound); + std::vector lines; + std::vector ::iterator it; + for (it = all.begin (); it != all.end (); ++it) + { + std::string line = *it; - trim (line); + // Strip comments + std::string::size_type pound = line.find ("#"); + if (pound != std::string::npos) + line = line.substr (0, pound); - // Skip blank lines - if (line.length () > 0) - lines.push_back (line); + trim (line); + + // Skip blank lines + if (line.length () > 0) + lines.push_back (line); + } + + // Take a guess at the file type. + fileType type = determineFileType (lines); + std::string identifier; + switch (type) + { + case task_1_4_3: identifier = "This looks like an older task export file."; break; + case task_1_5_0: identifier = "This looks like a recent task export file."; break; + case task_1_6_0: identifier = "This looks like a current task export file."; break; + case task_cmd_line: identifier = "This looks like task command line arguments."; break; + case todo_sh_2_0: identifier = "This looks like a todo.sh 2.x file."; break; + case csv: identifier = "This looks like a CSV file, but not a task export file."; break; + case text: identifier = "This looks like a text file with one task per line."; break; + case not_a_clue: + throw std::string ("Task cannot determine which type of file this is, " + "and cannot proceed."); + } + + // For tty users, confirm the import, as it is destructive. + if (isatty (fileno (stdout))) + if (! confirm (identifier + " Okay to proceed?")) + throw std::string ("Task will not import any data."); + + // Determine which type it might be, then attempt an import. + switch (type) + { + case task_1_4_3: out << importTask_1_4_3 (lines); break; + case task_1_5_0: out << importTask_1_5_0 (lines); break; + case task_1_6_0: out << importTask_1_6_0 (lines); break; + case task_cmd_line: out << importTaskCmdLine (lines); break; + case todo_sh_2_0: out << importTodoSh_2_0 (lines); break; + case csv: out << importCSV (lines); break; + case text: out << importText (lines); break; + case not_a_clue: /* to stop the compiler from complaining. */ break; + } } + else + throw std::string ("You must specify a file to import."); - // Take a guess at the file type. - fileType type = determineFileType (lines); - std::string identifier; - switch (type) - { - case task_1_4_3: identifier = "This looks like an older task export file."; break; - case task_1_5_0: identifier = "This looks like a recent task export file."; break; - case task_1_6_0: identifier = "This looks like a current task export file."; break; - case task_cmd_line: identifier = "This looks like task command line arguments."; break; - case todo_sh_2_0: identifier = "This looks like a todo.sh 2.x file."; break; - case csv: identifier = "This looks like a CSV file, but not a task export file."; break; - case text: identifier = "This looks like a text file with one task per line."; break; - case not_a_clue: - throw std::string ("Task cannot determine which type of file this is, " - "and cannot proceed."); - } - - // For tty users, confirm the import, as it is destructive. - if (isatty (fileno (stdout))) - if (! confirm (identifier + " Okay to proceed?")) - throw std::string ("Task will not import any data."); - - // Determine which type it might be, then attempt an import. - switch (type) - { - case task_1_4_3: out << importTask_1_4_3 (lines); break; - case task_1_5_0: out << importTask_1_5_0 (lines); break; - case task_1_6_0: out << importTask_1_6_0 (lines); break; - case task_cmd_line: out << importTaskCmdLine (lines); break; - case todo_sh_2_0: out << importTodoSh_2_0 (lines); break; - case csv: out << importCSV (lines); break; - case text: out << importText (lines); break; - case not_a_clue: /* to stop the compiler from complaining. */ break; - } + outs = out.str (); + context.hooks.trigger ("post-import-command"); } - else - throw std::string ("You must specify a file to import."); - outs = out.str (); - return 0; + return rc; } //////////////////////////////////////////////////////////////////////////////// diff --git a/src/report.cpp b/src/report.cpp index 8e3f2cb96..243b58c99 100644 --- a/src/report.cpp +++ b/src/report.cpp @@ -224,75 +224,82 @@ int shortUsage (std::string &outs) //////////////////////////////////////////////////////////////////////////////// int longUsage (std::string &outs) { - std::string shortUsageString; - std::stringstream out; + int rc = 0; - (void)shortUsage(shortUsageString); + if (context.hooks.trigger ("pre-usage-command")) + { + std::string shortUsageString; + std::stringstream out; - out << shortUsageString - << "ID is the numeric identifier displayed by the 'task list' command. " - << "You can specify multiple IDs for task commands, and multiple tasks " - << "will be affected. To specify multiple IDs make sure you use one " - << "of these forms:" << "\n" - << " task delete 1,2,3" << "\n" - << " task info 1-3" << "\n" - << " task pri:H 1,2-5,19" << "\n" - << "\n" - << "Tags are arbitrary words, any quantity:" << "\n" - << " +tag The + means add the tag" << "\n" - << " -tag The - means remove the tag" << "\n" - << "\n" - << "Attributes are:" << "\n" - << " project: Project name" << "\n" - << " priority: Priority" << "\n" - << " due: Due date" << "\n" - << " recur: Recurrence frequency" << "\n" - << " until: Recurrence end date" << "\n" - << " fg: Foreground color" << "\n" - << " bg: Background color" << "\n" - << " limit: Desired number of rows in report" << "\n" - << " wait: Date until task becomes pending" << "\n" - << "\n" - << "Attribute modifiers improve filters. Supported modifiers are:" << "\n" - << " before (synonyms under, below)" << "\n" - << " after (synonyms over, above)" << "\n" - << " none" << "\n" - << " any" << "\n" - << " is (synonym equals)" << "\n" - << " isnt (synonym not)" << "\n" - << " has (synonym contain)" << "\n" - << " hasnt" << "\n" - << " startswith (synonym left)" << "\n" - << " endswith (synonym right)" << "\n" - << " word" << "\n" - << " noword" << "\n" - << "\n" - << " For example:" << "\n" - << " task list due.before:eom priority.not:L" << "\n" - << "\n" - << "The default .taskrc file can be overridden with:" << "\n" - << " task rc: ..." << "\n" - << "\n" - << "The values in .taskrc (or alternate) can be overridden with:" << "\n" - << " task ... rc.:" << "\n" - << "\n" - << "Any command or attribute name may be abbreviated if still unique:" << "\n" - << " task list project:Home" << "\n" - << " task li pro:Home" << "\n" - << "\n" - << "Some task descriptions need to be escaped because of the shell:" << "\n" - << " task add \"quoted ' quote\"" << "\n" - << " task add escaped \\' quote" << "\n" - << "\n" - << "The argument -- tells task to treat all other args as description." << "\n" - << " task add -- project:Home needs scheduling" << "\n" - << "\n" - << "Many characters have special meaning to the shell, including:" << "\n" - << " $ ! ' \" ( ) ; \\ ` * ? { } [ ] < > | & % # ~" << "\n" - << std::endl; + (void)shortUsage(shortUsageString); - outs = out.str(); - return 0; + out << shortUsageString + << "ID is the numeric identifier displayed by the 'task list' command. " + << "You can specify multiple IDs for task commands, and multiple tasks " + << "will be affected. To specify multiple IDs make sure you use one " + << "of these forms:" << "\n" + << " task delete 1,2,3" << "\n" + << " task info 1-3" << "\n" + << " task pri:H 1,2-5,19" << "\n" + << "\n" + << "Tags are arbitrary words, any quantity:" << "\n" + << " +tag The + means add the tag" << "\n" + << " -tag The - means remove the tag" << "\n" + << "\n" + << "Attributes are:" << "\n" + << " project: Project name" << "\n" + << " priority: Priority" << "\n" + << " due: Due date" << "\n" + << " recur: Recurrence frequency" << "\n" + << " until: Recurrence end date" << "\n" + << " fg: Foreground color" << "\n" + << " bg: Background color" << "\n" + << " limit: Desired number of rows in report" << "\n" + << " wait: Date until task becomes pending" << "\n" + << "\n" + << "Attribute modifiers improve filters. Supported modifiers are:" << "\n" + << " before (synonyms under, below)" << "\n" + << " after (synonyms over, above)" << "\n" + << " none" << "\n" + << " any" << "\n" + << " is (synonym equals)" << "\n" + << " isnt (synonym not)" << "\n" + << " has (synonym contain)" << "\n" + << " hasnt" << "\n" + << " startswith (synonym left)" << "\n" + << " endswith (synonym right)" << "\n" + << " word" << "\n" + << " noword" << "\n" + << "\n" + << " For example:" << "\n" + << " task list due.before:eom priority.not:L" << "\n" + << "\n" + << "The default .taskrc file can be overridden with:" << "\n" + << " task rc: ..." << "\n" + << "\n" + << "The values in .taskrc (or alternate) can be overridden with:" << "\n" + << " task ... rc.:" << "\n" + << "\n" + << "Any command or attribute name may be abbreviated if still unique:" << "\n" + << " task list project:Home" << "\n" + << " task li pro:Home" << "\n" + << "\n" + << "Some task descriptions need to be escaped because of the shell:" << "\n" + << " task add \"quoted ' quote\"" << "\n" + << " task add escaped \\' quote" << "\n" + << "\n" + << "The argument -- tells task to treat all other args as description." << "\n" + << " task add -- project:Home needs scheduling" << "\n" + << "\n" + << "Many characters have special meaning to the shell, including:" << "\n" + << " $ ! ' \" ( ) ; \\ ` * ? { } [ ] < > | & % # ~" << "\n" + << std::endl; + + outs = out.str(); + context.hooks.trigger ("post-usage-command"); + } + + return rc; } //////////////////////////////////////////////////////////////////////////////// @@ -530,151 +537,157 @@ int handleInfo (std::string &outs) int handleReportSummary (std::string &outs) { int rc = 0; - // Scan the pending tasks. - std::vector tasks; - context.tdb.lock (context.config.getBoolean ("locking")); - handleRecurrence (); - context.tdb.load (tasks, context.filter); - context.tdb.commit (); - context.tdb.unlock (); - // Generate unique list of project names from all pending tasks. - std::map allProjects; - foreach (task, tasks) - if (task->getStatus () == Task::pending) - allProjects[task->get ("project")] = false; - - // Initialize counts, sum. - std::map countPending; - std::map countCompleted; - std::map sumEntry; - std::map counter; - time_t now = time (NULL); - - // Initialize counters. - foreach (project, allProjects) + if (context.hooks.trigger ("pre-summary-command")) { - countPending [project->first] = 0; - countCompleted [project->first] = 0; - sumEntry [project->first] = 0.0; - counter [project->first] = 0; - } + // Scan the pending tasks. + std::vector tasks; + context.tdb.lock (context.config.getBoolean ("locking")); + handleRecurrence (); + context.tdb.load (tasks, context.filter); + context.tdb.commit (); + context.tdb.unlock (); - // Count the various tasks. - foreach (task, tasks) - { - std::string project = task->get ("project"); - ++counter[project]; + // Generate unique list of project names from all pending tasks. + std::map allProjects; + foreach (task, tasks) + if (task->getStatus () == Task::pending) + allProjects[task->get ("project")] = false; - if (task->getStatus () == Task::pending || - task->getStatus () == Task::waiting) + // Initialize counts, sum. + std::map countPending; + std::map countCompleted; + std::map sumEntry; + std::map counter; + time_t now = time (NULL); + + // Initialize counters. + foreach (project, allProjects) { - ++countPending[project]; - - time_t entry = atoi (task->get ("entry").c_str ()); - if (entry) - sumEntry[project] = sumEntry[project] + (double) (now - entry); + countPending [project->first] = 0; + countCompleted [project->first] = 0; + sumEntry [project->first] = 0.0; + counter [project->first] = 0; } - else if (task->getStatus () == Task::completed) + // Count the various tasks. + foreach (task, tasks) { - ++countCompleted[project]; + std::string project = task->get ("project"); + ++counter[project]; - time_t entry = atoi (task->get ("entry").c_str ()); - time_t end = atoi (task->get ("end").c_str ()); - if (entry && end) - sumEntry[project] = sumEntry[project] + (double) (end - entry); + if (task->getStatus () == Task::pending || + task->getStatus () == Task::waiting) + { + ++countPending[project]; + + time_t entry = atoi (task->get ("entry").c_str ()); + if (entry) + sumEntry[project] = sumEntry[project] + (double) (now - entry); + } + + else if (task->getStatus () == Task::completed) + { + ++countCompleted[project]; + + time_t entry = atoi (task->get ("entry").c_str ()); + time_t end = atoi (task->get ("end").c_str ()); + if (entry && end) + sumEntry[project] = sumEntry[project] + (double) (end - entry); + } } - } - // Create a table for output. - Table table; - table.addColumn ("Project"); - table.addColumn ("Remaining"); - table.addColumn ("Avg age"); - table.addColumn ("Complete"); - table.addColumn ("0% 100%"); + // Create a table for output. + Table table; + table.addColumn ("Project"); + table.addColumn ("Remaining"); + table.addColumn ("Avg age"); + table.addColumn ("Complete"); + table.addColumn ("0% 100%"); - if ((context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor")) && - context.config.getBoolean ("fontunderline")) - { - table.setColumnUnderline (0); - table.setColumnUnderline (1); - table.setColumnUnderline (2); - table.setColumnUnderline (3); - } - else - table.setTableDashedUnderline (); - - table.setColumnJustification (1, Table::right); - table.setColumnJustification (2, Table::right); - table.setColumnJustification (3, Table::right); - - table.sortOn (0, Table::ascendingCharacter); - table.setDateFormat (context.config.get ("dateformat")); - - int barWidth = 30; - foreach (i, allProjects) - { - if (countPending[i->first] > 0) + if ((context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor")) && + context.config.getBoolean ("fontunderline")) { - int row = table.addRow (); - table.addCell (row, 0, (i->first == "" ? "(none)" : i->first)); - table.addCell (row, 1, countPending[i->first]); - if (counter[i->first]) - { - std::string age; - age = formatSeconds ((time_t) (sumEntry[i->first] / counter[i->first])); - table.addCell (row, 2, age); - } - - int c = countCompleted[i->first]; - int p = countPending[i->first]; - int completedBar = (c * barWidth) / (c + p); - - std::string bar; - if (context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor")) - { - bar = "\033[42m"; - for (int b = 0; b < completedBar; ++b) - bar += " "; - - bar += "\033[40m"; - for (int b = 0; b < barWidth - completedBar; ++b) - bar += " "; - - bar += "\033[0m"; - } - else - { - for (int b = 0; b < completedBar; ++b) - bar += "="; - - for (int b = 0; b < barWidth - completedBar; ++b) - bar += " "; - } - table.addCell (row, 4, bar); - - char percent[12]; - sprintf (percent, "%d%%", 100 * c / (c + p)); - table.addCell (row, 3, percent); + table.setColumnUnderline (0); + table.setColumnUnderline (1); + table.setColumnUnderline (2); + table.setColumnUnderline (3); } + else + table.setTableDashedUnderline (); + + table.setColumnJustification (1, Table::right); + table.setColumnJustification (2, Table::right); + table.setColumnJustification (3, Table::right); + + table.sortOn (0, Table::ascendingCharacter); + table.setDateFormat (context.config.get ("dateformat")); + + int barWidth = 30; + foreach (i, allProjects) + { + if (countPending[i->first] > 0) + { + int row = table.addRow (); + table.addCell (row, 0, (i->first == "" ? "(none)" : i->first)); + table.addCell (row, 1, countPending[i->first]); + if (counter[i->first]) + { + std::string age; + age = formatSeconds ((time_t) (sumEntry[i->first] / counter[i->first])); + table.addCell (row, 2, age); + } + + int c = countCompleted[i->first]; + int p = countPending[i->first]; + int completedBar = (c * barWidth) / (c + p); + + std::string bar; + if (context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor")) + { + bar = "\033[42m"; + for (int b = 0; b < completedBar; ++b) + bar += " "; + + bar += "\033[40m"; + for (int b = 0; b < barWidth - completedBar; ++b) + bar += " "; + + bar += "\033[0m"; + } + else + { + for (int b = 0; b < completedBar; ++b) + bar += "="; + + for (int b = 0; b < barWidth - completedBar; ++b) + bar += " "; + } + table.addCell (row, 4, bar); + + char percent[12]; + sprintf (percent, "%d%%", 100 * c / (c + p)); + table.addCell (row, 3, percent); + } + } + + std::stringstream out; + if (table.rowCount ()) + out << optionalBlankLine () + << table.render () + << optionalBlankLine () + << table.rowCount () + << (table.rowCount () == 1 ? " project" : " projects") + << std::endl; + else { + out << "No projects." << std::endl; + rc = 1; + } + + outs = out.str (); + context.hooks.trigger ("post-summary-command"); } - std::stringstream out; - if (table.rowCount ()) - out << optionalBlankLine () - << table.render () - << optionalBlankLine () - << table.rowCount () - << (table.rowCount () == 1 ? " project" : " projects") - << std::endl; - else { - out << "No projects." << std::endl; - rc = 1; - } - - outs = out.str (); return rc; } @@ -772,253 +785,85 @@ time_t monthlyEpoch (const std::string& date) int handleReportHistory (std::string &outs) { int rc = 0; - std::map groups; // Represents any month with data - std::map addedGroup; // Additions by month - std::map completedGroup; // Completions by month - std::map deletedGroup; // Deletions by month - // Scan the pending tasks. - std::vector tasks; - context.tdb.lock (context.config.getBoolean ("locking")); - handleRecurrence (); - context.tdb.load (tasks, context.filter); - context.tdb.commit (); - context.tdb.unlock (); - - foreach (task, tasks) + if (context.hooks.trigger ("pre-history-command")) { - time_t epoch = monthlyEpoch (task->get ("entry")); - groups[epoch] = 0; + std::map groups; // Represents any month with data + std::map addedGroup; // Additions by month + std::map completedGroup; // Completions by month + std::map deletedGroup; // Deletions by month - // Every task has an entry date. - if (addedGroup.find (epoch) != addedGroup.end ()) - addedGroup[epoch] = addedGroup[epoch] + 1; + // Scan the pending tasks. + std::vector tasks; + context.tdb.lock (context.config.getBoolean ("locking")); + handleRecurrence (); + context.tdb.load (tasks, context.filter); + context.tdb.commit (); + context.tdb.unlock (); + + foreach (task, tasks) + { + time_t epoch = monthlyEpoch (task->get ("entry")); + groups[epoch] = 0; + + // Every task has an entry date. + if (addedGroup.find (epoch) != addedGroup.end ()) + addedGroup[epoch] = addedGroup[epoch] + 1; + else + addedGroup[epoch] = 1; + + // All deleted tasks have an end date. + if (task->getStatus () == Task::deleted) + { + epoch = monthlyEpoch (task->get ("end")); + groups[epoch] = 0; + + if (deletedGroup.find (epoch) != deletedGroup.end ()) + deletedGroup[epoch] = deletedGroup[epoch] + 1; + else + deletedGroup[epoch] = 1; + } + + // All completed tasks have an end date. + else if (task->getStatus () == Task::completed) + { + epoch = monthlyEpoch (task->get ("end")); + groups[epoch] = 0; + + if (completedGroup.find (epoch) != completedGroup.end ()) + completedGroup[epoch] = completedGroup[epoch] + 1; + else + completedGroup[epoch] = 1; + } + } + + // Now build the table. + Table table; + table.setDateFormat (context.config.get ("dateformat")); + table.addColumn ("Year"); + table.addColumn ("Month"); + table.addColumn ("Added"); + table.addColumn ("Completed"); + table.addColumn ("Deleted"); + table.addColumn ("Net"); + + if ((context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor")) && + context.config.getBoolean ("fontunderline")) + { + table.setColumnUnderline (0); + table.setColumnUnderline (1); + table.setColumnUnderline (2); + table.setColumnUnderline (3); + table.setColumnUnderline (4); + table.setColumnUnderline (5); + } else - addedGroup[epoch] = 1; + table.setTableDashedUnderline (); - // All deleted tasks have an end date. - if (task->getStatus () == Task::deleted) - { - epoch = monthlyEpoch (task->get ("end")); - groups[epoch] = 0; - - if (deletedGroup.find (epoch) != deletedGroup.end ()) - deletedGroup[epoch] = deletedGroup[epoch] + 1; - else - deletedGroup[epoch] = 1; - } - - // All completed tasks have an end date. - else if (task->getStatus () == Task::completed) - { - epoch = monthlyEpoch (task->get ("end")); - groups[epoch] = 0; - - if (completedGroup.find (epoch) != completedGroup.end ()) - completedGroup[epoch] = completedGroup[epoch] + 1; - else - completedGroup[epoch] = 1; - } - } - - // Now build the table. - Table table; - table.setDateFormat (context.config.get ("dateformat")); - table.addColumn ("Year"); - table.addColumn ("Month"); - table.addColumn ("Added"); - table.addColumn ("Completed"); - table.addColumn ("Deleted"); - table.addColumn ("Net"); - - if ((context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor")) && - context.config.getBoolean ("fontunderline")) - { - table.setColumnUnderline (0); - table.setColumnUnderline (1); - table.setColumnUnderline (2); - table.setColumnUnderline (3); - table.setColumnUnderline (4); - table.setColumnUnderline (5); - } - else - table.setTableDashedUnderline (); - - table.setColumnJustification (2, Table::right); - table.setColumnJustification (3, Table::right); - table.setColumnJustification (4, Table::right); - table.setColumnJustification (5, Table::right); - - int totalAdded = 0; - int totalCompleted = 0; - int totalDeleted = 0; - - int priorYear = 0; - int row = 0; - foreach (i, groups) - { - row = table.addRow (); - - totalAdded += addedGroup [i->first]; - totalCompleted += completedGroup [i->first]; - totalDeleted += deletedGroup [i->first]; - - Date dt (i->first); - int m, d, y; - dt.toMDY (m, d, y); - - if (y != priorYear) - { - table.addCell (row, 0, y); - priorYear = y; - } - table.addCell (row, 1, Date::monthName(m)); - - int net = 0; - - if (addedGroup.find (i->first) != addedGroup.end ()) - { - table.addCell (row, 2, addedGroup[i->first]); - net +=addedGroup[i->first]; - } - - if (completedGroup.find (i->first) != completedGroup.end ()) - { - table.addCell (row, 3, completedGroup[i->first]); - net -= completedGroup[i->first]; - } - - if (deletedGroup.find (i->first) != deletedGroup.end ()) - { - table.addCell (row, 4, deletedGroup[i->first]); - net -= deletedGroup[i->first]; - } - - table.addCell (row, 5, net); - if ((context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor")) && net) - table.setCellColor (row, 5, net > 0 ? Color (Color::red) : - Color (Color::green)); - } - - if (table.rowCount ()) - { - table.addRow (); - row = table.addRow (); - - table.addCell (row, 1, "Average"); - if (context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor")) - table.setRowColor (row, Color (Color::nocolor, Color::nocolor, false, true, false)); - table.addCell (row, 2, totalAdded / (table.rowCount () - 2)); - table.addCell (row, 3, totalCompleted / (table.rowCount () - 2)); - table.addCell (row, 4, totalDeleted / (table.rowCount () - 2)); - table.addCell (row, 5, (totalAdded - totalCompleted - totalDeleted) / (table.rowCount () - 2)); - } - - std::stringstream out; - if (table.rowCount ()) - out << optionalBlankLine () - << table.render () - << std::endl; - else { - out << "No tasks." << std::endl; - rc = 1; - } - - outs = out.str (); - return rc; -} - -//////////////////////////////////////////////////////////////////////////////// -int handleReportGHistory (std::string &outs) -{ - int rc = 0; - std::map groups; // Represents any month with data - std::map addedGroup; // Additions by month - std::map completedGroup; // Completions by month - std::map deletedGroup; // Deletions by month - - // Scan the pending tasks. - std::vector tasks; - context.tdb.lock (context.config.getBoolean ("locking")); - handleRecurrence (); - context.tdb.load (tasks, context.filter); - context.tdb.commit (); - context.tdb.unlock (); - - foreach (task, tasks) - { - time_t epoch = monthlyEpoch (task->get ("entry")); - groups[epoch] = 0; - - // Every task has an entry date. - if (addedGroup.find (epoch) != addedGroup.end ()) - addedGroup[epoch] = addedGroup[epoch] + 1; - else - addedGroup[epoch] = 1; - - // All deleted tasks have an end date. - if (task->getStatus () == Task::deleted) - { - epoch = monthlyEpoch (task->get ("end")); - groups[epoch] = 0; - - if (deletedGroup.find (epoch) != deletedGroup.end ()) - deletedGroup[epoch] = deletedGroup[epoch] + 1; - else - deletedGroup[epoch] = 1; - } - - // All completed tasks have an end date. - else if (task->getStatus () == Task::completed) - { - epoch = monthlyEpoch (task->get ("end")); - groups[epoch] = 0; - - if (completedGroup.find (epoch) != completedGroup.end ()) - completedGroup[epoch] = completedGroup[epoch] + 1; - else - completedGroup[epoch] = 1; - } - } - - int widthOfBar = context.getWidth () - 15; // 15 == strlen ("2008 September ") - - // Now build the table. - Table table; - table.setDateFormat (context.config.get ("dateformat")); - table.addColumn ("Year"); - table.addColumn ("Month"); - table.addColumn ("Number Added/Completed/Deleted"); - - if ((context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor")) && - context.config.getBoolean ("fontunderline")) - { - table.setColumnUnderline (0); - table.setColumnUnderline (1); - } - else - table.setTableDashedUnderline (); - - Color color_added (Color::black, Color::red); - Color color_completed (Color::black, Color::green); - Color color_deleted (Color::black, Color::yellow); - - // Determine the longest line, and the longest "added" line. - int maxAddedLine = 0; - int maxRemovedLine = 0; - foreach (i, groups) - { - if (completedGroup[i->first] + deletedGroup[i->first] > maxRemovedLine) - maxRemovedLine = completedGroup[i->first] + deletedGroup[i->first]; - - if (addedGroup[i->first] > maxAddedLine) - maxAddedLine = addedGroup[i->first]; - } - - int maxLine = maxAddedLine + maxRemovedLine; - if (maxLine > 0) - { - unsigned int leftOffset = (widthOfBar * maxAddedLine) / maxLine; + table.setColumnJustification (2, Table::right); + table.setColumnJustification (3, Table::right); + table.setColumnJustification (4, Table::right); + table.setColumnJustification (5, Table::right); int totalAdded = 0; int totalCompleted = 0; @@ -1030,9 +875,9 @@ int handleReportGHistory (std::string &outs) { row = table.addRow (); - totalAdded += addedGroup[i->first]; - totalCompleted += completedGroup[i->first]; - totalDeleted += deletedGroup[i->first]; + totalAdded += addedGroup [i->first]; + totalCompleted += completedGroup [i->first]; + totalDeleted += deletedGroup [i->first]; Date dt (i->first); int m, d, y; @@ -1045,269 +890,458 @@ int handleReportGHistory (std::string &outs) } table.addCell (row, 1, Date::monthName(m)); - unsigned int addedBar = (widthOfBar * addedGroup[i->first]) / maxLine; - unsigned int completedBar = (widthOfBar * completedGroup[i->first]) / maxLine; - unsigned int deletedBar = (widthOfBar * deletedGroup[i->first]) / maxLine; + int net = 0; - std::string bar = ""; - if (context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor")) + if (addedGroup.find (i->first) != addedGroup.end ()) { - char number[24]; - std::string aBar = ""; - if (addedGroup[i->first]) - { - sprintf (number, "%d", addedGroup[i->first]); - aBar = number; - while (aBar.length () < addedBar) - aBar = " " + aBar; - } - - std::string cBar = ""; - if (completedGroup[i->first]) - { - sprintf (number, "%d", completedGroup[i->first]); - cBar = number; - while (cBar.length () < completedBar) - cBar = " " + cBar; - } - - std::string dBar = ""; - if (deletedGroup[i->first]) - { - sprintf (number, "%d", deletedGroup[i->first]); - dBar = number; - while (dBar.length () < deletedBar) - dBar = " " + dBar; - } - - while (bar.length () < leftOffset - aBar.length ()) - bar += " "; - - bar += color_added.colorize (aBar); - bar += color_completed.colorize (cBar); - bar += color_deleted.colorize (dBar); - } - else - { - std::string aBar = ""; while (aBar.length () < addedBar) aBar += "+"; - std::string cBar = ""; while (cBar.length () < completedBar) cBar += "X"; - std::string dBar = ""; while (dBar.length () < deletedBar) dBar += "-"; - - while (bar.length () < leftOffset - aBar.length ()) - bar += " "; - - bar += aBar + cBar + dBar; + table.addCell (row, 2, addedGroup[i->first]); + net +=addedGroup[i->first]; } - table.addCell (row, 2, bar); + if (completedGroup.find (i->first) != completedGroup.end ()) + { + table.addCell (row, 3, completedGroup[i->first]); + net -= completedGroup[i->first]; + } + + if (deletedGroup.find (i->first) != deletedGroup.end ()) + { + table.addCell (row, 4, deletedGroup[i->first]); + net -= deletedGroup[i->first]; + } + + table.addCell (row, 5, net); + if ((context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor")) && net) + table.setCellColor (row, 5, net > 0 ? Color (Color::red) : + Color (Color::green)); } - } - std::stringstream out; - if (table.rowCount ()) - { - out << optionalBlankLine () - << table.render () - << std::endl; + if (table.rowCount ()) + { + table.addRow (); + row = table.addRow (); - if (context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor")) - out << "Legend: " - << color_added.colorize ("added") - << ", " - << color_completed.colorize ("completed") - << ", " - << color_deleted.colorize ("deleted") - << optionalBlankLine () + table.addCell (row, 1, "Average"); + if (context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor")) + table.setRowColor (row, Color (Color::nocolor, Color::nocolor, false, true, false)); + table.addCell (row, 2, totalAdded / (table.rowCount () - 2)); + table.addCell (row, 3, totalCompleted / (table.rowCount () - 2)); + table.addCell (row, 4, totalDeleted / (table.rowCount () - 2)); + table.addCell (row, 5, (totalAdded - totalCompleted - totalDeleted) / (table.rowCount () - 2)); + } + + std::stringstream out; + if (table.rowCount ()) + out << optionalBlankLine () + << table.render () << std::endl; else - out << "Legend: + added, X completed, - deleted" << std::endl; - } - else { - out << "No tasks." << std::endl; - rc = 1; + { + out << "No tasks." << std::endl; + rc = 1; + } + + outs = out.str (); + context.hooks.trigger ("post-history-command"); + } + + return rc; +} + +//////////////////////////////////////////////////////////////////////////////// +int handleReportGHistory (std::string &outs) +{ + int rc = 0; + + if (context.hooks.trigger ("pre-ghistory-command")) + { + std::map groups; // Represents any month with data + std::map addedGroup; // Additions by month + std::map completedGroup; // Completions by month + std::map deletedGroup; // Deletions by month + + // Scan the pending tasks. + std::vector tasks; + context.tdb.lock (context.config.getBoolean ("locking")); + handleRecurrence (); + context.tdb.load (tasks, context.filter); + context.tdb.commit (); + context.tdb.unlock (); + + foreach (task, tasks) + { + time_t epoch = monthlyEpoch (task->get ("entry")); + groups[epoch] = 0; + + // Every task has an entry date. + if (addedGroup.find (epoch) != addedGroup.end ()) + addedGroup[epoch] = addedGroup[epoch] + 1; + else + addedGroup[epoch] = 1; + + // All deleted tasks have an end date. + if (task->getStatus () == Task::deleted) + { + epoch = monthlyEpoch (task->get ("end")); + groups[epoch] = 0; + + if (deletedGroup.find (epoch) != deletedGroup.end ()) + deletedGroup[epoch] = deletedGroup[epoch] + 1; + else + deletedGroup[epoch] = 1; + } + + // All completed tasks have an end date. + else if (task->getStatus () == Task::completed) + { + epoch = monthlyEpoch (task->get ("end")); + groups[epoch] = 0; + + if (completedGroup.find (epoch) != completedGroup.end ()) + completedGroup[epoch] = completedGroup[epoch] + 1; + else + completedGroup[epoch] = 1; + } + } + + int widthOfBar = context.getWidth () - 15; // 15 == strlen ("2008 September ") + + // Now build the table. + Table table; + table.setDateFormat (context.config.get ("dateformat")); + table.addColumn ("Year"); + table.addColumn ("Month"); + table.addColumn ("Number Added/Completed/Deleted"); + + if ((context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor")) && + context.config.getBoolean ("fontunderline")) + { + table.setColumnUnderline (0); + table.setColumnUnderline (1); + } + else + table.setTableDashedUnderline (); + + Color color_added (Color::black, Color::red); + Color color_completed (Color::black, Color::green); + Color color_deleted (Color::black, Color::yellow); + + // Determine the longest line, and the longest "added" line. + int maxAddedLine = 0; + int maxRemovedLine = 0; + foreach (i, groups) + { + if (completedGroup[i->first] + deletedGroup[i->first] > maxRemovedLine) + maxRemovedLine = completedGroup[i->first] + deletedGroup[i->first]; + + if (addedGroup[i->first] > maxAddedLine) + maxAddedLine = addedGroup[i->first]; + } + + int maxLine = maxAddedLine + maxRemovedLine; + if (maxLine > 0) + { + unsigned int leftOffset = (widthOfBar * maxAddedLine) / maxLine; + + int totalAdded = 0; + int totalCompleted = 0; + int totalDeleted = 0; + + int priorYear = 0; + int row = 0; + foreach (i, groups) + { + row = table.addRow (); + + totalAdded += addedGroup[i->first]; + totalCompleted += completedGroup[i->first]; + totalDeleted += deletedGroup[i->first]; + + Date dt (i->first); + int m, d, y; + dt.toMDY (m, d, y); + + if (y != priorYear) + { + table.addCell (row, 0, y); + priorYear = y; + } + table.addCell (row, 1, Date::monthName(m)); + + unsigned int addedBar = (widthOfBar * addedGroup[i->first]) / maxLine; + unsigned int completedBar = (widthOfBar * completedGroup[i->first]) / maxLine; + unsigned int deletedBar = (widthOfBar * deletedGroup[i->first]) / maxLine; + + std::string bar = ""; + if (context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor")) + { + char number[24]; + std::string aBar = ""; + if (addedGroup[i->first]) + { + sprintf (number, "%d", addedGroup[i->first]); + aBar = number; + while (aBar.length () < addedBar) + aBar = " " + aBar; + } + + std::string cBar = ""; + if (completedGroup[i->first]) + { + sprintf (number, "%d", completedGroup[i->first]); + cBar = number; + while (cBar.length () < completedBar) + cBar = " " + cBar; + } + + std::string dBar = ""; + if (deletedGroup[i->first]) + { + sprintf (number, "%d", deletedGroup[i->first]); + dBar = number; + while (dBar.length () < deletedBar) + dBar = " " + dBar; + } + + while (bar.length () < leftOffset - aBar.length ()) + bar += " "; + + bar += color_added.colorize (aBar); + bar += color_completed.colorize (cBar); + bar += color_deleted.colorize (dBar); + } + else + { + std::string aBar = ""; while (aBar.length () < addedBar) aBar += "+"; + std::string cBar = ""; while (cBar.length () < completedBar) cBar += "X"; + std::string dBar = ""; while (dBar.length () < deletedBar) dBar += "-"; + + while (bar.length () < leftOffset - aBar.length ()) + bar += " "; + + bar += aBar + cBar + dBar; + } + + table.addCell (row, 2, bar); + } + } + + std::stringstream out; + if (table.rowCount ()) + { + out << optionalBlankLine () + << table.render () + << std::endl; + + if (context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor")) + out << "Legend: " + << color_added.colorize ("added") + << ", " + << color_completed.colorize ("completed") + << ", " + << color_deleted.colorize ("deleted") + << optionalBlankLine () + << std::endl; + else + out << "Legend: + added, X completed, - deleted" << std::endl; + } + else + { + out << "No tasks." << std::endl; + rc = 1; + } + + outs = out.str (); + context.hooks.trigger ("post-ghistory-command"); } - outs = out.str (); return rc; } //////////////////////////////////////////////////////////////////////////////// int handleReportTimesheet (std::string &outs) { - // Scan the pending tasks. - std::vector tasks; - context.tdb.lock (context.config.getBoolean ("locking")); - handleRecurrence (); - context.tdb.load (tasks, context.filter); - context.tdb.commit (); - context.tdb.unlock (); + int rc = 0; - // Just do this once. - int width = context.getWidth (); - - // What day of the week does the user consider the first? - int weekStart = Date::dayOfWeek (context.config.get ("weekstart")); - if (weekStart != 0 && weekStart != 1) - throw std::string ("The 'weekstart' configuration variable may " - "only contain 'Sunday' or 'Monday'."); - - // Determine the date of the first day of the most recent report. - Date today; - Date start; - start -= (((today.dayOfWeek () - weekStart) + 7) % 7) * 86400; - - // Roll back to midnight. - start = Date (start.month (), start.day (), start.year ()); - Date end = start + (7 * 86400); - - // Determine how many reports to run. - int quantity = 1; - if (context.sequence.size () == 1) - quantity = context.sequence[0]; - - bool color = context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor"); - - std::stringstream out; - for (int week = 0; week < quantity; ++week) + if (context.hooks.trigger ("pre-timesheet-command")) { - Date endString (end); - endString -= 86400; + // Scan the pending tasks. + std::vector tasks; + context.tdb.lock (context.config.getBoolean ("locking")); + handleRecurrence (); + context.tdb.load (tasks, context.filter); + context.tdb.commit (); + context.tdb.unlock (); - std::string title = start.toString (context.config.get ("dateformat")) - + " - " - + endString.toString (context.config.get ("dateformat")); + // Just do this once. + int width = context.getWidth (); - Color bold (Color::nocolor, Color::nocolor, false, true, false); - out << std::endl - << (color ? bold.colorize (title) : title) - << std::endl; + // What day of the week does the user consider the first? + int weekStart = Date::dayOfWeek (context.config.get ("weekstart")); + if (weekStart != 0 && weekStart != 1) + throw std::string ("The 'weekstart' configuration variable may " + "only contain 'Sunday' or 'Monday'."); - // Render the completed table. - Table completed; - completed.setTableWidth (width); - completed.addColumn (" "); - completed.addColumn ("Project"); - completed.addColumn ("Due"); - completed.addColumn ("Description"); + // Determine the date of the first day of the most recent report. + Date today; + Date start; + start -= (((today.dayOfWeek () - weekStart) + 7) % 7) * 86400; - if (color && context.config.getBoolean ("fontunderline")) + // Roll back to midnight. + start = Date (start.month (), start.day (), start.year ()); + Date end = start + (7 * 86400); + + // Determine how many reports to run. + int quantity = 1; + if (context.sequence.size () == 1) + quantity = context.sequence[0]; + + bool color = context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor"); + + std::stringstream out; + for (int week = 0; week < quantity; ++week) { - completed.setColumnUnderline (1); - completed.setColumnUnderline (2); - completed.setColumnUnderline (3); - } - else - completed.setTableDashedUnderline (); + Date endString (end); + endString -= 86400; - completed.setColumnWidth (0, Table::minimum); - completed.setColumnWidth (1, Table::minimum); - completed.setColumnWidth (2, Table::minimum); - completed.setColumnWidth (3, Table::flexible); + std::string title = start.toString (context.config.get ("dateformat")) + + " - " + + endString.toString (context.config.get ("dateformat")); - completed.setColumnJustification (0, Table::left); - completed.setColumnJustification (1, Table::left); - completed.setColumnJustification (2, Table::right); - completed.setColumnJustification (3, Table::left); + Color bold (Color::nocolor, Color::nocolor, false, true, false); + out << std::endl + << (color ? bold.colorize (title) : title) + << std::endl; - foreach (task, tasks) - { - // If task completed within range. - if (task->getStatus () == Task::completed) + // Render the completed table. + Table completed; + completed.setTableWidth (width); + completed.addColumn (" "); + completed.addColumn ("Project"); + completed.addColumn ("Due"); + completed.addColumn ("Description"); + + if (color && context.config.getBoolean ("fontunderline")) { - Date compDate (atoi (task->get ("end").c_str ())); - if (compDate >= start && compDate < end) - { - int row = completed.addRow (); - std::string format = context.config.get ("dateformat.report"); - if (format == "") - format = context.config.get ("dateformat"); - completed.addCell (row, 1, task->get ("project")); - completed.addCell (row, 2, getDueDate (*task, format)); - completed.addCell (row, 3, getFullDescription (*task, "timesheet")); + completed.setColumnUnderline (1); + completed.setColumnUnderline (2); + completed.setColumnUnderline (3); + } + else + completed.setTableDashedUnderline (); - if (color) + completed.setColumnWidth (0, Table::minimum); + completed.setColumnWidth (1, Table::minimum); + completed.setColumnWidth (2, Table::minimum); + completed.setColumnWidth (3, Table::flexible); + + completed.setColumnJustification (0, Table::left); + completed.setColumnJustification (1, Table::left); + completed.setColumnJustification (2, Table::right); + completed.setColumnJustification (3, Table::left); + + foreach (task, tasks) + { + // If task completed within range. + if (task->getStatus () == Task::completed) + { + Date compDate (atoi (task->get ("end").c_str ())); + if (compDate >= start && compDate < end) { - Color c (task->get ("fg") + " " + task->get ("bg")); - autoColorize (*task, c); - completed.setRowColor (row, c); + int row = completed.addRow (); + std::string format = context.config.get ("dateformat.report"); + if (format == "") + format = context.config.get ("dateformat"); + completed.addCell (row, 1, task->get ("project")); + completed.addCell (row, 2, getDueDate (*task, format)); + completed.addCell (row, 3, getFullDescription (*task, "timesheet")); + + if (color) + { + Color c (task->get ("fg") + " " + task->get ("bg")); + autoColorize (*task, c); + completed.setRowColor (row, c); + } } } } - } - out << " Completed (" << completed.rowCount () << " tasks)" << std::endl; + out << " Completed (" << completed.rowCount () << " tasks)" << std::endl; - if (completed.rowCount ()) - out << completed.render () - << std::endl; + if (completed.rowCount ()) + out << completed.render () + << std::endl; - // Now render the started table. - Table started; - started.setTableWidth (width); - started.addColumn (" "); - started.addColumn ("Project"); - started.addColumn ("Due"); - started.addColumn ("Description"); + // Now render the started table. + Table started; + started.setTableWidth (width); + started.addColumn (" "); + started.addColumn ("Project"); + started.addColumn ("Due"); + started.addColumn ("Description"); - if (color && context.config.getBoolean ("fontunderline")) - { - completed.setColumnUnderline (1); - completed.setColumnUnderline (2); - completed.setColumnUnderline (3); - } - else - completed.setTableDashedUnderline (); - - started.setColumnWidth (0, Table::minimum); - started.setColumnWidth (1, Table::minimum); - started.setColumnWidth (2, Table::minimum); - started.setColumnWidth (3, Table::flexible); - - started.setColumnJustification (0, Table::left); - started.setColumnJustification (1, Table::left); - started.setColumnJustification (2, Table::right); - started.setColumnJustification (3, Table::left); - foreach (task, tasks) - { - // If task started within range, but not completed withing range. - if (task->getStatus () == Task::pending && - task->has ("start")) + if (color && context.config.getBoolean ("fontunderline")) { - Date startDate (atoi (task->get ("start").c_str ())); - if (startDate >= start && startDate < end) - { - int row = started.addRow (); - std::string format = context.config.get ("dateformat.report"); - if (format == "") - format = context.config.get ("dateformat"); - started.addCell (row, 1, task->get ("project")); - started.addCell (row, 2, getDueDate (*task, format)); - started.addCell (row, 3, getFullDescription (*task, "timesheet")); + completed.setColumnUnderline (1); + completed.setColumnUnderline (2); + completed.setColumnUnderline (3); + } + else + completed.setTableDashedUnderline (); - if (color) + started.setColumnWidth (0, Table::minimum); + started.setColumnWidth (1, Table::minimum); + started.setColumnWidth (2, Table::minimum); + started.setColumnWidth (3, Table::flexible); + + started.setColumnJustification (0, Table::left); + started.setColumnJustification (1, Table::left); + started.setColumnJustification (2, Table::right); + started.setColumnJustification (3, Table::left); + foreach (task, tasks) + { + // If task started within range, but not completed withing range. + if (task->getStatus () == Task::pending && + task->has ("start")) + { + Date startDate (atoi (task->get ("start").c_str ())); + if (startDate >= start && startDate < end) { - Color c (task->get ("fg") + " " + task->get ("bg")); - autoColorize (*task, c); - started.setRowColor (row, c); + int row = started.addRow (); + std::string format = context.config.get ("dateformat.report"); + if (format == "") + format = context.config.get ("dateformat"); + started.addCell (row, 1, task->get ("project")); + started.addCell (row, 2, getDueDate (*task, format)); + started.addCell (row, 3, getFullDescription (*task, "timesheet")); + + if (color) + { + Color c (task->get ("fg") + " " + task->get ("bg")); + autoColorize (*task, c); + started.setRowColor (row, c); + } } } } + + out << " Started (" << started.rowCount () << " tasks)" << std::endl; + + if (started.rowCount ()) + out << started.render () + << std::endl + << std::endl; + + // Prior week. + start -= 7 * 86400; + end -= 7 * 86400; } - out << " Started (" << started.rowCount () << " tasks)" << std::endl; - - if (started.rowCount ()) - out << started.render () - << std::endl - << std::endl; - - // Prior week. - start -= 7 * 86400; - end -= 7 * 86400; + outs = out.str (); + context.hooks.trigger ("post-timesheet-command"); } - outs = out.str (); - return 0; + return rc; } //////////////////////////////////////////////////////////////////////////////// @@ -1512,525 +1546,539 @@ std::string renderMonths ( //////////////////////////////////////////////////////////////////////////////// int handleReportCalendar (std::string &outs) { - // Each month requires 28 text columns width. See how many will actually - // fit. But if a preference is specified, and it fits, use it. - int width = context.getWidth (); - int preferredMonthsPerLine = (context.config.getInteger ("monthsperline")); - int monthsThatFit = width / 26; + int rc = 0; - int monthsPerLine = monthsThatFit; - if (preferredMonthsPerLine != 0 && preferredMonthsPerLine < monthsThatFit) - monthsPerLine = preferredMonthsPerLine; + if (context.hooks.trigger ("pre-calendar-command")) + { + // Each month requires 28 text columns width. See how many will actually + // fit. But if a preference is specified, and it fits, use it. + int width = context.getWidth (); + int preferredMonthsPerLine = (context.config.getInteger ("monthsperline")); + int monthsThatFit = width / 26; - // Get all the tasks. - std::vector tasks; - Filter filter; - context.tdb.lock (context.config.getBoolean ("locking")); - handleRecurrence (); - context.tdb.loadPending (tasks, filter); - context.tdb.commit (); - context.tdb.unlock (); + int monthsPerLine = monthsThatFit; + if (preferredMonthsPerLine != 0 && preferredMonthsPerLine < monthsThatFit) + monthsPerLine = preferredMonthsPerLine; - Date today; - bool getpendingdate = false; - int monthsToDisplay = 1; - int mFrom = today.month (); - int yFrom = today.year (); - int mTo = mFrom; - int yTo = yFrom; + // Get all the tasks. + std::vector tasks; + Filter filter; + context.tdb.lock (context.config.getBoolean ("locking")); + handleRecurrence (); + context.tdb.loadPending (tasks, filter); + context.tdb.commit (); + context.tdb.unlock (); - // Determine what to do - int numberOfArgs = context.args.size(); + Date today; + bool getpendingdate = false; + int monthsToDisplay = 1; + int mFrom = today.month (); + int yFrom = today.year (); + int mTo = mFrom; + int yTo = yFrom; - if (numberOfArgs == 1) { - // task cal - monthsToDisplay = monthsPerLine; - mFrom = today.month(); - yFrom = today.year(); - } - else if (numberOfArgs == 2) { - if (context.args[1] == "y") { - // task cal y - monthsToDisplay = 12; + // Determine what to do + int numberOfArgs = context.args.size(); + + if (numberOfArgs == 1) { + // task cal + monthsToDisplay = monthsPerLine; mFrom = today.month(); yFrom = today.year(); } - else if (context.args[1] == "due") { - // task cal due - monthsToDisplay = monthsPerLine; - getpendingdate = true; + else if (numberOfArgs == 2) { + if (context.args[1] == "y") { + // task cal y + monthsToDisplay = 12; + mFrom = today.month(); + yFrom = today.year(); + } + else if (context.args[1] == "due") { + // task cal due + monthsToDisplay = monthsPerLine; + getpendingdate = true; + } + else { + // task cal 2010 + monthsToDisplay = 12; + mFrom = 1; + yFrom = atoi (context.args[1].c_str ()); + } } - else { - // task cal 2010 + else if (numberOfArgs == 3) { + if (context.args[2] == "y") { + // task cal due y + monthsToDisplay = 12; + getpendingdate = true; + } + else { + // task cal 8 2010 + monthsToDisplay = monthsPerLine; + mFrom = atoi (context.args[1].c_str ()); + yFrom = atoi (context.args[2].c_str ()); + } + } + else if (numberOfArgs == 4) { + // task cal 8 2010 y monthsToDisplay = 12; - mFrom = 1; - yFrom = atoi (context.args[1].c_str ()); - } - } - else if (numberOfArgs == 3) { - if (context.args[2] == "y") { - // task cal due y - monthsToDisplay = 12; - getpendingdate = true; - } - else { - // task cal 8 2010 - monthsToDisplay = monthsPerLine; mFrom = atoi (context.args[1].c_str ()); yFrom = atoi (context.args[2].c_str ()); } - } - else if (numberOfArgs == 4) { - // task cal 8 2010 y - monthsToDisplay = 12; - mFrom = atoi (context.args[1].c_str ()); - yFrom = atoi (context.args[2].c_str ()); - } - int countDueDates = 0; - if (getpendingdate == true) { - // Find the oldest pending due date. - Date oldest (12,31,2037); - foreach (task, tasks) - { - if (task->getStatus () == Task::pending) + int countDueDates = 0; + if (getpendingdate == true) { + // Find the oldest pending due date. + Date oldest (12,31,2037); + foreach (task, tasks) { - if (task->has ("due")) + if (task->getStatus () == Task::pending) { - ++countDueDates; - Date d (atoi (task->get ("due").c_str ())); - if (d < oldest) oldest = d; + if (task->has ("due")) + { + ++countDueDates; + Date d (atoi (task->get ("due").c_str ())); + if (d < oldest) oldest = d; + } } } + mFrom = oldest.month(); + yFrom = oldest.year(); } - mFrom = oldest.month(); - yFrom = oldest.year(); - } - mTo = mFrom + monthsToDisplay - 1; - yTo = yFrom; - if (mTo > 12) { - mTo -=12; - yTo++; - } + mTo = mFrom + monthsToDisplay - 1; + yTo = yFrom; + if (mTo > 12) { + mTo -=12; + yTo++; + } - int details_yFrom = yFrom; - int details_mFrom = mFrom; + int details_yFrom = yFrom; + int details_mFrom = mFrom; - std::stringstream out; - out << std::endl; + std::stringstream out; + out << std::endl; - while (yFrom < yTo || (yFrom == yTo && mFrom <= mTo)) - { - int nextM = mFrom; - int nextY = yFrom; - - // Print month headers (cheating on the width settings, yes) - for (int i = 0 ; i < monthsPerLine ; i++) + while (yFrom < yTo || (yFrom == yTo && mFrom <= mTo)) { - std::string month = Date::monthName (nextM); + int nextM = mFrom; + int nextY = yFrom; - // 12345678901234567890123456 = 26 chars wide - // ^^ = center - // <-------> = 13 - (month.length / 2) + 1 - // <------> = 26 - above - // +--------------------------+ - // | July 2009 | - // | Mo Tu We Th Fr Sa Su | - // | 27 1 2 3 4 5 | - // | 28 6 7 8 9 10 11 12 | - // | 29 13 14 15 16 17 18 19 | - // | 30 20 21 22 23 24 25 26 | - // | 31 27 28 29 30 31 | - // +--------------------------+ - - int totalWidth = 26; - int labelWidth = month.length () + 5; // 5 = " 2009" - int leftGap = (totalWidth / 2) - (labelWidth / 2); - int rightGap = totalWidth - leftGap - labelWidth; - - out << std::setw (leftGap) << ' ' - << month - << ' ' - << nextY - << std::setw (rightGap) << ' '; - - if (++nextM > 12) + // Print month headers (cheating on the width settings, yes) + for (int i = 0 ; i < monthsPerLine ; i++) { - nextM = 1; - nextY++; + std::string month = Date::monthName (nextM); + + // 12345678901234567890123456 = 26 chars wide + // ^^ = center + // <-------> = 13 - (month.length / 2) + 1 + // <------> = 26 - above + // +--------------------------+ + // | July 2009 | + // | Mo Tu We Th Fr Sa Su | + // | 27 1 2 3 4 5 | + // | 28 6 7 8 9 10 11 12 | + // | 29 13 14 15 16 17 18 19 | + // | 30 20 21 22 23 24 25 26 | + // | 31 27 28 29 30 31 | + // +--------------------------+ + + int totalWidth = 26; + int labelWidth = month.length () + 5; // 5 = " 2009" + int leftGap = (totalWidth / 2) - (labelWidth / 2); + int rightGap = totalWidth - leftGap - labelWidth; + + out << std::setw (leftGap) << ' ' + << month + << ' ' + << nextY + << std::setw (rightGap) << ' '; + + if (++nextM > 12) + { + nextM = 1; + nextY++; + } } - } - out << std::endl - << optionalBlankLine () - << renderMonths (mFrom, yFrom, today, tasks, monthsPerLine) - << std::endl; - - mFrom += monthsPerLine; - if (mFrom > 12) - { - mFrom -= 12; - ++yFrom; - } - } - - Color color_today (context.config.get ("color.calendar.today")); - Color color_due (context.config.get ("color.calendar.due")); - Color color_overdue (context.config.get ("color.calendar.overdue")); - Color color_weekend (context.config.get ("color.calendar.weekend")); - Color color_holiday (context.config.get ("color.calendar.holiday")); - Color color_weeknumber (context.config.get ("color.calendar.weeknumber")); - - if ((context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor")) && - context.config.getBoolean ("calendar.legend")) - out << "Legend: " - << color_today.colorize ("today") - << ", " - << color_due.colorize ("due") - << ", " - << color_overdue.colorize ("overdue") - << ", " - << color_weekend.colorize ("weekend") - << ", " - << color_holiday.colorize ("holiday") - << ", " - << color_weeknumber.colorize ("weeknumber") - << "." - << optionalBlankLine () - << std::endl; - - if (context.config.get ("calendar.details") == "full" || context.config.get ("calendar.holidays") == "full") - { - --details_mFrom; - if (details_mFrom == 0) - { - details_mFrom = 12; - --details_yFrom; - } - int details_dFrom = Date::daysInMonth (details_mFrom, details_yFrom); - - ++mTo; - if (mTo == 13) - { - mTo = 1; - ++yTo; - } - - Date date_after (details_mFrom, details_dFrom, details_yFrom); - std::string after = date_after.toString (context.config.get ("dateformat")); - - Date date_before (mTo, 1, yTo); - std::string before = date_before.toString (context.config.get ("dateformat")); - - // Table with due date information - if (context.config.get ("calendar.details") == "full") - { - std::string report = context.config.get ("calendar.details.report"); - std::string report_filter = context.config.get ("report." + report + ".filter"); - - report_filter += " due.after:" + after + " due.before:" + before; - context.config.set ("report." + report + ".filter", report_filter); - - // Display all due task in the report colorized not only the imminet ones - context.config.set ("due", 0); - - context.args.clear (); - context.filter.clear (); - context.sequence.clear (); - - std::string output; - handleCustomReport (report, output); - out << output; - } - - // Table with holiday information - if (context.config.get ("calendar.holidays") == "full") - { - std::vector holidays; - context.config.all (holidays); - - Table holTable; - holTable.setTableWidth (context.getWidth ()); - holTable.addColumn ("Date"); - holTable.addColumn ("Holiday"); - holTable.sortOn (0, Table::ascendingDueDate); - - if ((context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor")) && - context.config.getBoolean ("fontunderline")) - { - holTable.setColumnUnderline (0); - holTable.setColumnUnderline (1); - } - else - holTable.setTableDashedUnderline (); - - holTable.setColumnWidth (0, Table::minimum); - holTable.setColumnWidth (1, Table::flexible); - - holTable.setColumnJustification (0, Table::left); - holTable.setColumnJustification (1, Table::left); - - foreach (hol, holidays) - if (hol->substr (0, 8) == "holiday.") - if (hol->substr (hol->size () - 4) == "name") - { - std::string holName = context.config.get ("holiday." + hol->substr (8, hol->size () - 13) + ".name"); - std::string holDate = context.config.get ("holiday." + hol->substr (8, hol->size () - 13) + ".date"); - Date hDate (holDate.c_str (), context.config.get ("dateformat.holiday")); - - if (date_after < hDate && hDate < date_before) - { - std::string format = context.config.get ("report." + - context.config.get ("calendar.details.report") + - ".dateformat"); - if (format == "") - format = context.config.get ("dateformat.report"); - if (format == "") - format = context.config.get ("dateformat"); - - int row = holTable.addRow (); - holTable.addCell (row, 0, hDate.toString (format)); - holTable.addCell (row, 1, holName); - } - } - out << optionalBlankLine () - << holTable.render () + out << std::endl + << optionalBlankLine () + << renderMonths (mFrom, yFrom, today, tasks, monthsPerLine) << std::endl; + + mFrom += monthsPerLine; + if (mFrom > 12) + { + mFrom -= 12; + ++yFrom; + } } + + Color color_today (context.config.get ("color.calendar.today")); + Color color_due (context.config.get ("color.calendar.due")); + Color color_overdue (context.config.get ("color.calendar.overdue")); + Color color_weekend (context.config.get ("color.calendar.weekend")); + Color color_holiday (context.config.get ("color.calendar.holiday")); + Color color_weeknumber (context.config.get ("color.calendar.weeknumber")); + + if ((context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor")) && + context.config.getBoolean ("calendar.legend")) + out << "Legend: " + << color_today.colorize ("today") + << ", " + << color_due.colorize ("due") + << ", " + << color_overdue.colorize ("overdue") + << ", " + << color_weekend.colorize ("weekend") + << ", " + << color_holiday.colorize ("holiday") + << ", " + << color_weeknumber.colorize ("weeknumber") + << "." + << optionalBlankLine () + << std::endl; + + if (context.config.get ("calendar.details") == "full" || context.config.get ("calendar.holidays") == "full") + { + --details_mFrom; + if (details_mFrom == 0) + { + details_mFrom = 12; + --details_yFrom; + } + int details_dFrom = Date::daysInMonth (details_mFrom, details_yFrom); + + ++mTo; + if (mTo == 13) + { + mTo = 1; + ++yTo; + } + + Date date_after (details_mFrom, details_dFrom, details_yFrom); + std::string after = date_after.toString (context.config.get ("dateformat")); + + Date date_before (mTo, 1, yTo); + std::string before = date_before.toString (context.config.get ("dateformat")); + + // Table with due date information + if (context.config.get ("calendar.details") == "full") + { + std::string report = context.config.get ("calendar.details.report"); + std::string report_filter = context.config.get ("report." + report + ".filter"); + + report_filter += " due.after:" + after + " due.before:" + before; + context.config.set ("report." + report + ".filter", report_filter); + + // Display all due task in the report colorized not only the imminet ones + context.config.set ("due", 0); + + context.args.clear (); + context.filter.clear (); + context.sequence.clear (); + + std::string output; + handleCustomReport (report, output); + out << output; + } + + // Table with holiday information + if (context.config.get ("calendar.holidays") == "full") + { + std::vector holidays; + context.config.all (holidays); + + Table holTable; + holTable.setTableWidth (context.getWidth ()); + holTable.addColumn ("Date"); + holTable.addColumn ("Holiday"); + holTable.sortOn (0, Table::ascendingDueDate); + + if ((context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor")) && + context.config.getBoolean ("fontunderline")) + { + holTable.setColumnUnderline (0); + holTable.setColumnUnderline (1); + } + else + holTable.setTableDashedUnderline (); + + holTable.setColumnWidth (0, Table::minimum); + holTable.setColumnWidth (1, Table::flexible); + + holTable.setColumnJustification (0, Table::left); + holTable.setColumnJustification (1, Table::left); + + foreach (hol, holidays) + if (hol->substr (0, 8) == "holiday.") + if (hol->substr (hol->size () - 4) == "name") + { + std::string holName = context.config.get ("holiday." + hol->substr (8, hol->size () - 13) + ".name"); + std::string holDate = context.config.get ("holiday." + hol->substr (8, hol->size () - 13) + ".date"); + Date hDate (holDate.c_str (), context.config.get ("dateformat.holiday")); + + if (date_after < hDate && hDate < date_before) + { + std::string format = context.config.get ("report." + + context.config.get ("calendar.details.report") + + ".dateformat"); + if (format == "") + format = context.config.get ("dateformat.report"); + if (format == "") + format = context.config.get ("dateformat"); + + int row = holTable.addRow (); + holTable.addCell (row, 0, hDate.toString (format)); + holTable.addCell (row, 1, holName); + } + } + out << optionalBlankLine () + << holTable.render () + << std::endl; + } + } + + outs = out.str (); + context.hooks.trigger ("post-calendar-command"); } - outs = out.str (); - return 0; + return rc; } //////////////////////////////////////////////////////////////////////////////// int handleReportStats (std::string &outs) { - std::stringstream out; + int rc = 0; - // Go get the file sizes. - size_t dataSize = 0; - - Directory location (context.config.get ("data.location")); - File pending (location.data + "/pending.data"); - dataSize += pending.size (); - - File completed (location.data + "/completed.data"); - dataSize += completed.size (); - - File undo (location.data + "/undo.data"); - dataSize += undo.size (); - - std::vector undoTxns; - File::read (undo, undoTxns); - int undoCount = 0; - foreach (tx, undoTxns) - if (tx->substr (0, 3) == "---") - ++undoCount; - - // Get all the tasks. - std::vector tasks; - context.tdb.lock (context.config.getBoolean ("locking")); - handleRecurrence (); - context.tdb.load (tasks, context.filter); - context.tdb.commit (); - context.tdb.unlock (); - - Date now; - time_t earliest = time (NULL); - time_t latest = 1; - int totalT = 0; - int deletedT = 0; - int pendingT = 0; - int completedT = 0; - int waitingT = 0; - int taggedT = 0; - int annotationsT = 0; - int recurringT = 0; - float daysPending = 0.0; - int descLength = 0; - std::map allTags; - std::map allProjects; - - std::vector ::iterator it; - for (it = tasks.begin (); it != tasks.end (); ++it) + if (context.hooks.trigger ("pre-stats-command")) { - ++totalT; - if (it->getStatus () == Task::deleted) ++deletedT; - if (it->getStatus () == Task::pending) ++pendingT; - if (it->getStatus () == Task::completed) ++completedT; - if (it->getStatus () == Task::recurring) ++recurringT; - if (it->getStatus () == Task::waiting) ++waitingT; + std::stringstream out; - time_t entry = atoi (it->get ("entry").c_str ()); - if (entry < earliest) earliest = entry; - if (entry > latest) latest = entry; + // Go get the file sizes. + size_t dataSize = 0; - if (it->getStatus () == Task::completed) + Directory location (context.config.get ("data.location")); + File pending (location.data + "/pending.data"); + dataSize += pending.size (); + + File completed (location.data + "/completed.data"); + dataSize += completed.size (); + + File undo (location.data + "/undo.data"); + dataSize += undo.size (); + + std::vector undoTxns; + File::read (undo, undoTxns); + int undoCount = 0; + foreach (tx, undoTxns) + if (tx->substr (0, 3) == "---") + ++undoCount; + + // Get all the tasks. + std::vector tasks; + context.tdb.lock (context.config.getBoolean ("locking")); + handleRecurrence (); + context.tdb.load (tasks, context.filter); + context.tdb.commit (); + context.tdb.unlock (); + + Date now; + time_t earliest = time (NULL); + time_t latest = 1; + int totalT = 0; + int deletedT = 0; + int pendingT = 0; + int completedT = 0; + int waitingT = 0; + int taggedT = 0; + int annotationsT = 0; + int recurringT = 0; + float daysPending = 0.0; + int descLength = 0; + std::map allTags; + std::map allProjects; + + std::vector ::iterator it; + for (it = tasks.begin (); it != tasks.end (); ++it) { - time_t end = atoi (it->get ("end").c_str ()); - daysPending += (end - entry) / 86400.0; + ++totalT; + if (it->getStatus () == Task::deleted) ++deletedT; + if (it->getStatus () == Task::pending) ++pendingT; + if (it->getStatus () == Task::completed) ++completedT; + if (it->getStatus () == Task::recurring) ++recurringT; + if (it->getStatus () == Task::waiting) ++waitingT; + + time_t entry = atoi (it->get ("entry").c_str ()); + if (entry < earliest) earliest = entry; + if (entry > latest) latest = entry; + + if (it->getStatus () == Task::completed) + { + time_t end = atoi (it->get ("end").c_str ()); + daysPending += (end - entry) / 86400.0; + } + + if (it->getStatus () == Task::pending) + daysPending += (now - entry) / 86400.0; + + descLength += it->get ("description").length (); + + std::vector annotations; + it->getAnnotations (annotations); + annotationsT += annotations.size (); + + std::vector tags; + it->getTags (tags); + if (tags.size ()) ++taggedT; + + foreach (t, tags) + allTags[*t] = 0; + + std::string project = it->get ("project"); + if (project != "") + allProjects[project] = 0; } - if (it->getStatus () == Task::pending) - daysPending += (now - entry) / 86400.0; + // Create a table for output. + Table table; + table.setTableWidth (context.getWidth ()); + table.setTableIntraPadding (2); + table.setDateFormat (context.config.get ("dateformat")); + table.addColumn ("Category"); + table.addColumn ("Data"); - descLength += it->get ("description").length (); + if ((context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor")) && + context.config.getBoolean ("fontunderline")) + { + table.setColumnUnderline (0); + table.setColumnUnderline (1); + } + else + table.setTableDashedUnderline (); - std::vector annotations; - it->getAnnotations (annotations); - annotationsT += annotations.size (); + table.setColumnWidth (0, Table::minimum); + table.setColumnWidth (1, Table::flexible); - std::vector tags; - it->getTags (tags); - if (tags.size ()) ++taggedT; + table.setColumnJustification (0, Table::left); + table.setColumnJustification (1, Table::left); - foreach (t, tags) - allTags[*t] = 0; - - std::string project = it->get ("project"); - if (project != "") - allProjects[project] = 0; - } - - // Create a table for output. - Table table; - table.setTableWidth (context.getWidth ()); - table.setTableIntraPadding (2); - table.setDateFormat (context.config.get ("dateformat")); - table.addColumn ("Category"); - table.addColumn ("Data"); - - if ((context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor")) && - context.config.getBoolean ("fontunderline")) - { - table.setColumnUnderline (0); - table.setColumnUnderline (1); - } - else - table.setTableDashedUnderline (); - - table.setColumnWidth (0, Table::minimum); - table.setColumnWidth (1, Table::flexible); - - table.setColumnJustification (0, Table::left); - table.setColumnJustification (1, Table::left); - - int row = table.addRow (); - table.addCell (row, 0, "Pending"); - table.addCell (row, 1, pendingT); - - row = table.addRow (); - table.addCell (row, 0, "Waiting"); - table.addCell (row, 1, waitingT); - - row = table.addRow (); - table.addCell (row, 0, "Recurring"); - table.addCell (row, 1, recurringT); - - row = table.addRow (); - table.addCell (row, 0, "Completed"); - table.addCell (row, 1, completedT); - - row = table.addRow (); - table.addCell (row, 0, "Deleted"); - table.addCell (row, 1, deletedT); - - row = table.addRow (); - table.addCell (row, 0, "Total"); - table.addCell (row, 1, totalT); - - row = table.addRow (); - table.addCell (row, 0, "Annotations"); - table.addCell (row, 1, annotationsT); - - row = table.addRow (); - table.addCell (row, 0, "Unique tags"); - table.addCell (row, 1, (int)allTags.size ()); - - row = table.addRow (); - table.addCell (row, 0, "Projects"); - table.addCell (row, 1, (int)allProjects.size ()); - - row = table.addRow (); - table.addCell (row, 0, "Data size"); - table.addCell (row, 1, formatBytes (dataSize)); - - row = table.addRow (); - table.addCell (row, 0, "Undo transactions"); - table.addCell (row, 1, undoCount); - - if (totalT) - { - row = table.addRow (); - table.addCell (row, 0, "Tasks tagged"); - - std::stringstream value; - value << std::setprecision (3) << (100.0 * taggedT / totalT) << "%"; - table.addCell (row, 1, value.str ()); - } - - if (tasks.size ()) - { - Date e (earliest); - row = table.addRow (); - table.addCell (row, 0, "Oldest task"); - table.addCell (row, 1, e.toString (context.config.get ("dateformat"))); - - Date l (latest); - row = table.addRow (); - table.addCell (row, 0, "Newest task"); - table.addCell (row, 1, l.toString (context.config.get ("dateformat"))); + int row = table.addRow (); + table.addCell (row, 0, "Pending"); + table.addCell (row, 1, pendingT); row = table.addRow (); - table.addCell (row, 0, "Task used for"); - table.addCell (row, 1, formatSeconds (latest - earliest)); - } + table.addCell (row, 0, "Waiting"); + table.addCell (row, 1, waitingT); - if (totalT) - { row = table.addRow (); - table.addCell (row, 0, "Task added every"); - table.addCell (row, 1, formatSeconds ((latest - earliest) / totalT)); - } + table.addCell (row, 0, "Recurring"); + table.addCell (row, 1, recurringT); - if (completedT) - { row = table.addRow (); - table.addCell (row, 0, "Task completed every"); - table.addCell (row, 1, formatSeconds ((latest - earliest) / completedT)); - } + table.addCell (row, 0, "Completed"); + table.addCell (row, 1, completedT); - if (deletedT) - { row = table.addRow (); - table.addCell (row, 0, "Task deleted every"); - table.addCell (row, 1, formatSeconds ((latest - earliest) / deletedT)); - } + table.addCell (row, 0, "Deleted"); + table.addCell (row, 1, deletedT); - if (pendingT || completedT) - { row = table.addRow (); - table.addCell (row, 0, "Average time pending"); - table.addCell (row, 1, formatSeconds ((int) ((daysPending / (pendingT + completedT)) * 86400))); - } + table.addCell (row, 0, "Total"); + table.addCell (row, 1, totalT); - if (totalT) - { row = table.addRow (); - table.addCell (row, 0, "Average desc length"); - std::stringstream value; - value << (int) (descLength / totalT) << " characters"; - table.addCell (row, 1, value.str ()); + table.addCell (row, 0, "Annotations"); + table.addCell (row, 1, annotationsT); + + row = table.addRow (); + table.addCell (row, 0, "Unique tags"); + table.addCell (row, 1, (int)allTags.size ()); + + row = table.addRow (); + table.addCell (row, 0, "Projects"); + table.addCell (row, 1, (int)allProjects.size ()); + + row = table.addRow (); + table.addCell (row, 0, "Data size"); + table.addCell (row, 1, formatBytes (dataSize)); + + row = table.addRow (); + table.addCell (row, 0, "Undo transactions"); + table.addCell (row, 1, undoCount); + + if (totalT) + { + row = table.addRow (); + table.addCell (row, 0, "Tasks tagged"); + + std::stringstream value; + value << std::setprecision (3) << (100.0 * taggedT / totalT) << "%"; + table.addCell (row, 1, value.str ()); + } + + if (tasks.size ()) + { + Date e (earliest); + row = table.addRow (); + table.addCell (row, 0, "Oldest task"); + table.addCell (row, 1, e.toString (context.config.get ("dateformat"))); + + Date l (latest); + row = table.addRow (); + table.addCell (row, 0, "Newest task"); + table.addCell (row, 1, l.toString (context.config.get ("dateformat"))); + + row = table.addRow (); + table.addCell (row, 0, "Task used for"); + table.addCell (row, 1, formatSeconds (latest - earliest)); + } + + if (totalT) + { + row = table.addRow (); + table.addCell (row, 0, "Task added every"); + table.addCell (row, 1, formatSeconds ((latest - earliest) / totalT)); + } + + if (completedT) + { + row = table.addRow (); + table.addCell (row, 0, "Task completed every"); + table.addCell (row, 1, formatSeconds ((latest - earliest) / completedT)); + } + + if (deletedT) + { + row = table.addRow (); + table.addCell (row, 0, "Task deleted every"); + table.addCell (row, 1, formatSeconds ((latest - earliest) / deletedT)); + } + + if (pendingT || completedT) + { + row = table.addRow (); + table.addCell (row, 0, "Average time pending"); + table.addCell (row, 1, formatSeconds ((int) ((daysPending / (pendingT + completedT)) * 86400))); + } + + if (totalT) + { + row = table.addRow (); + table.addCell (row, 0, "Average desc length"); + std::stringstream value; + value << (int) (descLength / totalT) << " characters"; + table.addCell (row, 1, value.str ()); + } + + out << optionalBlankLine () + << table.render () + << optionalBlankLine (); + + outs = out.str (); + context.hooks.trigger ("post-stats-command"); } - out << optionalBlankLine () - << table.render () - << optionalBlankLine (); - - outs = out.str (); - return 0; + return rc; } ////////////////////////////////////////////////////////////////////////////////