diff --git a/src/Makefile.am b/src/Makefile.am index 64586d1c7..eae651e2b 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -1,3 +1,3 @@ bin_PROGRAMS = task -task_SOURCES = Config.cpp Date.cpp T.cpp TDB.cpp Table.cpp Grid.cpp color.cpp parse.cpp task.cpp util.cpp text.cpp rules.cpp Config.h Date.h T.h TDB.h Table.h Grid.h color.h task.h +task_SOURCES = Config.cpp Date.cpp T.cpp TDB.cpp Table.cpp Grid.cpp color.cpp parse.cpp task.cpp command.cpp report.cpp util.cpp text.cpp rules.cpp Config.h Date.h T.h TDB.h Table.h Grid.h color.h task.h AM_CPPFLAGS = -Wall -pedantic -ggdb3 -fno-rtti diff --git a/src/T.cpp b/src/T.cpp index 815b39c28..8ccb6c362 100644 --- a/src/T.cpp +++ b/src/T.cpp @@ -424,7 +424,8 @@ void T::parse (const std::string& line) mStatus = line[37] == '+' ? completed : line[37] == 'X' ? deleted - : pending; + : line[37] == 'r' ? recurring + : pending; size_t openTagBracket = line.find ("["); size_t closeTagBracket = line.find ("]", openTagBracket); diff --git a/src/TDB.cpp b/src/TDB.cpp index d80067644..fc5e8e786 100644 --- a/src/TDB.cpp +++ b/src/TDB.cpp @@ -468,7 +468,8 @@ int TDB::gc () const for (it = all.begin (); it != all.end (); ++it) { // Some tasks stay in the pending file. - if (it->getStatus () == T::pending) + if (it->getStatus () == T::pending || + it->getStatus () == T::recurring) pending.push_back (*it); // Others are transferred to the completed file. diff --git a/src/command.cpp b/src/command.cpp new file mode 100644 index 000000000..edea54777 --- /dev/null +++ b/src/command.cpp @@ -0,0 +1,589 @@ +//////////////////////////////////////////////////////////////////////////////// +// task - a command line task list manager. +// +// Copyright 2006 - 2008, Paul Beckingham. +// All rights reserved. +// +// This program is free software; you can redistribute it and/or modify it under +// the terms of the GNU General Public License as published by the Free Software +// Foundation; either version 2 of the License, or (at your option) any later +// version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +// details. +// +// You should have received a copy of the GNU General Public License along with +// this program; if not, write to the +// +// Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, +// Boston, MA +// 02110-1301 +// USA +// +//////////////////////////////////////////////////////////////////////////////// +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Config.h" +#include "Date.h" +#include "Table.h" +#include "TDB.h" +#include "T.h" +#include "task.h" + +#ifdef HAVE_LIBNCURSES +#include +#endif + +//////////////////////////////////////////////////////////////////////////////// +void handleAdd (const TDB& tdb, T& task, Config& conf) +{ + char entryTime[16]; + sprintf (entryTime, "%u", (unsigned int) time (NULL)); + task.setAttribute ("entry", entryTime); + + std::map atts; + task.getAttributes (atts); + foreach (i, atts) + { + if (i->second == "") + task.removeAttribute (i->first); + } + + // Recurring tasks get a special status. + if (task.getAttribute ("due") != "" && + task.getAttribute ("recur") != "") + { + task.setStatus (T::recurring); + task.setAttribute ("mask", ""); + } + + if (task.getDescription () == "") + throw std::string ("Cannot add a blank task."); + + if (!tdb.addT (task)) + throw std::string ("Could not create new task."); +} + +//////////////////////////////////////////////////////////////////////////////// +void handleProjects (const TDB& tdb, T& task, Config& conf) +{ + // Get all the tasks, including deleted ones. + std::vector tasks; + tdb.pendingT (tasks); + + // Scan all the tasks for their project name, building a map using project + // names as keys. + std::map unique; + for (unsigned int i = 0; i < tasks.size (); ++i) + { + T task (tasks[i]); + unique[task.getAttribute ("project")] += 1; + } + + if (unique.size ()) + { + // Render a list of project names from the map. + Table table; + table.addColumn ("Project"); + table.addColumn ("Tasks"); + + if (conf.get ("color", true)) + { + table.setColumnUnderline (0); + table.setColumnUnderline (1); + } + + table.setColumnJustification (1, Table::right); + table.setDateFormat (conf.get ("dateformat", "m/d/Y")); + + foreach (i, unique) + { + int row = table.addRow (); + table.addCell (row, 0, i->first); + table.addCell (row, 1, i->second); + } + + std::cout << optionalBlankLine (conf) + << table.render () + << optionalBlankLine (conf) + << unique.size () + << (unique.size () == 1 ? " project" : " projects") + << std::endl; + } + else + std::cout << "No projects." + << std::endl; +} + +//////////////////////////////////////////////////////////////////////////////// +void handleTags (const TDB& tdb, T& task, Config& conf) +{ + // Get all the tasks. + std::vector tasks; + tdb.pendingT (tasks); + + // Scan all the tasks for their project name, building a map using project + // names as keys. + std::map unique; + for (unsigned int i = 0; i < tasks.size (); ++i) + { + T task (tasks[i]); + + std::vector tags; + task.getTags (tags); + + for (unsigned int t = 0; t < tags.size (); ++t) + unique[tags[t]] = ""; + } + + // Render a list of tag names from the map. + std::cout << optionalBlankLine (conf); + foreach (i, unique) + std::cout << i->first << std::endl; + + if (unique.size ()) + std::cout << optionalBlankLine (conf) + << unique.size () + << (unique.size () == 1 ? " tag" : " tags") + << std::endl; + else + std::cout << "No tags." + << std::endl; +} + +//////////////////////////////////////////////////////////////////////////////// +// If a task is deleted, but is still in the pending file, then it may be +// undeleted simply by changing it's status. +void handleUndelete (const TDB& tdb, T& task, Config& conf) +{ + std::vector all; + tdb.allPendingT (all); + + int id = task.getId (); + std::vector ::iterator it; + for (it = all.begin (); it != all.end (); ++it) + { + if (it->getId () == id) + { + if (it->getStatus () == T::deleted) + { + if (it->getAttribute ("recur") != "") + { + std::cout << "Task does not support 'undelete' for recurring tasks." << std::endl; + return; + } + + T restored (*it); + restored.setStatus (T::pending); + restored.removeAttribute ("end"); + tdb.modifyT (restored); + + std::cout << "Task " << id << " successfully undeleted." << std::endl; + return; + } + else + { + std::cout << "Task " << id << " is not deleted - therefore cannot undelete." << std::endl; + return; + } + } + } + + std::cout << "Task " << id + << " not found - tasks can only be reliably undeleted if the undelete" << std::endl + << "command is run immediately after the errant delete command." << std::endl; +} + +//////////////////////////////////////////////////////////////////////////////// +void handleVersion (Config& conf) +{ + // Determine window size, and set table accordingly. + int width = conf.get ("defaultwidth", 80); +#ifdef HAVE_LIBNCURSES + if (conf.get ("curses", true)) + { + WINDOW* w = initscr (); + width = w->_maxx + 1; + endwin (); + } +#endif + + // Create a table for output. + Table table; + table.setTableWidth (width); + table.setDateFormat (conf.get ("dateformat", "m/d/Y")); + table.addColumn ("Config variable"); + table.addColumn ("Value"); + + if (conf.get ("color", true)) + { + table.setColumnUnderline (0); + table.setColumnUnderline (1); + } + + 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); + + std::vector all; + conf.all (all); + foreach (i, all) + { + std::string value = conf.get (*i); + if (value != "") + { + int row = table.addRow (); + table.addCell (row, 0, *i); + table.addCell (row, 1, value); + } + } + + std::cout << "Copyright (C) 2006 - 2008, P. Beckingham." + << std::endl + << PACKAGE + << " " + << VERSION + << std::endl + << std::endl + << "Task comes with ABSOLUTELY NO WARRANTY; for details read the COPYING file" + << std::endl + << "included. This is free software, and you are welcome to redistribute it" + << std::endl + << "under certain conditions; again, see the COPYING file for details." + << std::endl + << std::endl + << table.render () + << std::endl; + + // Verify installation. This is mentioned in the documentation as the way to + // ensure everything is properly installed. + + if (all.size () == 0) + std::cout << "Configuration error: .taskrc contains no entries" + << std::endl; + else + { + if (conf.get ("data.location") == "") + std::cout << "Configuration error: data.location not specified in .taskrc " + "file." + << std::endl; + + if (access (conf.get ("data.location").c_str (), X_OK)) + std::cout << "Configuration error: data.location contains a directory name" + " that doesn't exist, or is unreadable." + << std::endl; + } +} + +//////////////////////////////////////////////////////////////////////////////// +void handleDelete (const TDB& tdb, T& task, Config& conf) +{ + if (conf.get ("confirmation") != "yes" || confirm ("Permanently delete task?")) + { + // Check for the more complex case of a recurring task. If this is a + // recurring task, get confirmation to delete them all. + std::string parent = task.getAttribute ("parent"); + if (parent != "") + { + if (confirm ("This is a recurring task. Do you want to delete all pending recurrences of this same task?")) + { + // Scan all pending tasks for siblings of this task, and the parent + // itself, and delete them. + std::vector all; + tdb.allPendingT (all); + std::vector ::iterator it; + for (it = all.begin (); it != all.end (); ++it) + if (it->getAttribute ("parent") == parent || + it->getUUID () == parent) + tdb.deleteT (*it); + + return; + } + else + { + // TODO Update mask in parent. + } + } + + // No confirmation, just delete the one. + tdb.deleteT (task); + } + else + std::cout << "Task not deleted." << std::endl; +} + +//////////////////////////////////////////////////////////////////////////////// +void handleStart (const TDB& tdb, T& task, Config& conf) +{ + std::vector all; + tdb.pendingT (all); + + std::vector ::iterator it; + for (it = all.begin (); it != all.end (); ++it) + { + if (it->getId () == task.getId ()) + { + T original (*it); + + if (original.getAttribute ("start") == "") + { + char startTime[16]; + sprintf (startTime, "%u", (unsigned int) time (NULL)); + original.setAttribute ("start", startTime); + + original.setId (task.getId ()); + tdb.modifyT (original); + + nag (tdb, task, conf); + return; + } + else + std::cout << "Task " << task.getId () << " already started." << std::endl; + } + } + + throw std::string ("Task not found."); +} + +//////////////////////////////////////////////////////////////////////////////// +void handleDone (const TDB& tdb, T& task, Config& conf) +{ + if (!tdb.completeT (task)) + throw std::string ("Could not mark task as completed."); + + // TODO Now updates mask in parent. + + nag (tdb, task, conf); +} + +//////////////////////////////////////////////////////////////////////////////// +void handleExport (const TDB& tdb, T& task, Config& conf) +{ + // Use the description as a file name, then clobber the description so the + // file name isn't used for filtering. + std::string file = trim (task.getDescription ()); + task.setDescription (""); + + if (file.length () > 0) + { + std::ofstream out (file.c_str ()); + if (out.good ()) + { + out << "'id'," + << "'status'," + << "'tags'," + << "'entry'," + << "'start'," + << "'due'," + << "'end'," + << "'project'," + << "'priority'," + << "'fg'," + << "'bg'," + << "'description'" + << "\n"; + + std::vector all; + tdb.allT (all); + filter (all, task); + foreach (t, all) + { + out << t->composeCSV ().c_str (); + } + out.close (); + } + else + throw std::string ("Could not write to export file."); + } + else + throw std::string ("You must specify a file to write to."); +} + +//////////////////////////////////////////////////////////////////////////////// +void handleModify (const TDB& tdb, T& task, Config& conf) +{ + std::vector all; + tdb.pendingT (all); + + std::vector ::iterator it; + for (it = all.begin (); it != all.end (); ++it) + { + if (it->getId () == task.getId ()) + { + T original (*it); + + // A non-zero value forces a file write. + int changes = 0; + + // Apply a new description, if any. + if (task.getDescription () != "") + { + original.setDescription (task.getDescription ()); + ++changes; + } + + // Apply or remove tags, if any. + std::vector tags; + task.getTags (tags); + for (unsigned int i = 0; i < tags.size (); ++i) + { + if (tags[i][0] == '+') + original.addTag (tags[i].substr (1, std::string::npos)); + else + original.addTag (tags[i]); + + ++changes; + } + + task.getRemoveTags (tags); + for (unsigned int i = 0; i < tags.size (); ++i) + { + if (tags[i][0] == '-') + original.removeTag (tags[i].substr (1, std::string::npos)); + else + original.removeTag (tags[i]); + + ++changes; + } + + // Apply or remove attributes, if any. + std::map attributes; + task.getAttributes (attributes); + foreach (i, attributes) + { + if (i->second == "") + original.removeAttribute (i->first); + else + original.setAttribute (i->first, i->second); + + ++changes; + } + + std::string from; + std::string to; + task.getSubstitution (from, to); + if (from != "") + { + std::string description = original.getDescription (); + size_t pattern = description.find (from); + if (pattern != std::string::npos) + { + description = description.substr (0, pattern) + + to + + description.substr (pattern + from.length (), std::string::npos); + original.setDescription (description); + ++changes; + } + } + + if (changes) + { + original.setId (task.getId ()); + tdb.modifyT (original); + } + + return; + } + } + + throw std::string ("Task not found."); +} + +//////////////////////////////////////////////////////////////////////////////// +void handleColor (Config& conf) +{ + if (conf.get ("color", true)) + { + std::cout << optionalBlankLine (conf) << "Foreground" << std::endl + << " " + << Text::colorize (Text::bold, Text::nocolor, "bold") << " " + << Text::colorize (Text::underline, Text::nocolor, "underline") << " " + << Text::colorize (Text::bold_underline, Text::nocolor, "bold_underline") << std::endl + + << " " << Text::colorize (Text::black, Text::nocolor, "black") << " " + << Text::colorize (Text::bold_black, Text::nocolor, "bold_black") << " " + << Text::colorize (Text::underline_black, Text::nocolor, "underline_black") << " " + << Text::colorize (Text::bold_underline_black, Text::nocolor, "bold_underline_black") << std::endl + + << " " << Text::colorize (Text::red, Text::nocolor, "red") << " " + << Text::colorize (Text::bold_red, Text::nocolor, "bold_red") << " " + << Text::colorize (Text::underline_red, Text::nocolor, "underline_red") << " " + << Text::colorize (Text::bold_underline_red, Text::nocolor, "bold_underline_red") << std::endl + + << " " << Text::colorize (Text::green, Text::nocolor, "green") << " " + << Text::colorize (Text::bold_green, Text::nocolor, "bold_green") << " " + << Text::colorize (Text::underline_green, Text::nocolor, "underline_green") << " " + << Text::colorize (Text::bold_underline_green, Text::nocolor, "bold_underline_green") << std::endl + + << " " << Text::colorize (Text::yellow, Text::nocolor, "yellow") << " " + << Text::colorize (Text::bold_yellow, Text::nocolor, "bold_yellow") << " " + << Text::colorize (Text::underline_yellow, Text::nocolor, "underline_yellow") << " " + << Text::colorize (Text::bold_underline_yellow, Text::nocolor, "bold_underline_yellow") << std::endl + + << " " << Text::colorize (Text::blue, Text::nocolor, "blue") << " " + << Text::colorize (Text::bold_blue, Text::nocolor, "bold_blue") << " " + << Text::colorize (Text::underline_blue, Text::nocolor, "underline_blue") << " " + << Text::colorize (Text::bold_underline_blue, Text::nocolor, "bold_underline_blue") << std::endl + + << " " << Text::colorize (Text::magenta, Text::nocolor, "magenta") << " " + << Text::colorize (Text::bold_magenta, Text::nocolor, "bold_magenta") << " " + << Text::colorize (Text::underline_magenta, Text::nocolor, "underline_magenta") << " " + << Text::colorize (Text::bold_underline_magenta, Text::nocolor, "bold_underline_magenta") << std::endl + + << " " << Text::colorize (Text::cyan, Text::nocolor, "cyan") << " " + << Text::colorize (Text::bold_cyan, Text::nocolor, "bold_cyan") << " " + << Text::colorize (Text::underline_cyan, Text::nocolor, "underline_cyan") << " " + << Text::colorize (Text::bold_underline_cyan, Text::nocolor, "bold_underline_cyan") << std::endl + + << " " << Text::colorize (Text::white, Text::nocolor, "white") << " " + << Text::colorize (Text::bold_white, Text::nocolor, "bold_white") << " " + << Text::colorize (Text::underline_white, Text::nocolor, "underline_white") << " " + << Text::colorize (Text::bold_underline_white, Text::nocolor, "bold_underline_white") << std::endl + + << std::endl << "Background" << std::endl + << " " << Text::colorize (Text::nocolor, Text::on_black, "on_black") << " " + << Text::colorize (Text::nocolor, Text::on_bright_black, "on_bright_black") << std::endl + + << " " << Text::colorize (Text::nocolor, Text::on_red, "on_red") << " " + << Text::colorize (Text::nocolor, Text::on_bright_red, "on_bright_red") << std::endl + + << " " << Text::colorize (Text::nocolor, Text::on_green, "on_green") << " " + << Text::colorize (Text::nocolor, Text::on_bright_green, "on_bright_green") << std::endl + + << " " << Text::colorize (Text::nocolor, Text::on_yellow, "on_yellow") << " " + << Text::colorize (Text::nocolor, Text::on_bright_yellow, "on_bright_yellow") << std::endl + + << " " << Text::colorize (Text::nocolor, Text::on_blue, "on_blue") << " " + << Text::colorize (Text::nocolor, Text::on_bright_blue, "on_bright_blue") << std::endl + + << " " << Text::colorize (Text::nocolor, Text::on_magenta, "on_magenta") << " " + << Text::colorize (Text::nocolor, Text::on_bright_magenta, "on_bright_magenta") << std::endl + + << " " << Text::colorize (Text::nocolor, Text::on_cyan, "on_cyan") << " " + << Text::colorize (Text::nocolor, Text::on_bright_cyan, "on_bright_cyan") << std::endl + + << " " << Text::colorize (Text::nocolor, Text::on_white, "on_white") << " " + << Text::colorize (Text::nocolor, Text::on_bright_white, "on_bright_white") << std::endl + + << optionalBlankLine (conf); + } + else + { + std::cout << "Color is currently turned off in your .taskrc file." << std::endl; + } +} + +//////////////////////////////////////////////////////////////////////////////// diff --git a/src/parse.cpp b/src/parse.cpp index 4f01d1fd1..2506fb9fd 100644 --- a/src/parse.cpp +++ b/src/parse.cpp @@ -109,6 +109,7 @@ static const char* attributes[] = "end", "recur", "until", + "mask", "", }; @@ -231,6 +232,9 @@ static bool validAttribute ( else if (name == "due" && value != "") validDate (value, conf); + else if (name == "until" && value != "") + validDate (value, conf); + else if (name == "priority") { value = upperCase (value); @@ -240,7 +244,8 @@ static bool validAttribute ( // Some attributes are intended to be private. else if (name == "entry" || name == "start" || - name == "end") + name == "end" || + name == "mask") throw std::string ("\"") + name + "\" is not an attribute you may modify directly."; @@ -316,9 +321,9 @@ static bool validSubstitution ( } //////////////////////////////////////////////////////////////////////////////// -bool validDuration (const std::string& input) +bool validDuration (std::string& input) { - return convertDuration (input) != 0 ? true : false; + return (convertDuration (input) != 0) ? true : false; } //////////////////////////////////////////////////////////////////////////////// diff --git a/src/report.cpp b/src/report.cpp new file mode 100644 index 000000000..0362e8458 --- /dev/null +++ b/src/report.cpp @@ -0,0 +1,2623 @@ +//////////////////////////////////////////////////////////////////////////////// +// task - a command line task list manager. +// +// Copyright 2006 - 2008, Paul Beckingham. +// All rights reserved. +// +// This program is free software; you can redistribute it and/or modify it under +// the terms of the GNU General Public License as published by the Free Software +// Foundation; either version 2 of the License, or (at your option) any later +// version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +// details. +// +// You should have received a copy of the GNU General Public License along with +// this program; if not, write to the +// +// Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, +// Boston, MA +// 02110-1301 +// USA +// +//////////////////////////////////////////////////////////////////////////////// +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Config.h" +#include "Date.h" +#include "Table.h" +#include "TDB.h" +#include "T.h" +#include "task.h" + +#ifdef HAVE_LIBNCURSES +#include +#endif + +//////////////////////////////////////////////////////////////////////////////// +void filter (std::vector& all, T& task) +{ + std::vector filtered; + + // Split any description specified into words. + std::vector descWords; + split (descWords, lowerCase (task.getDescription ()), ' '); + + // Get all the tags to match against. + std::vector tagList; + task.getTags (tagList); + + // Get all the attributes to match against. + std::map attrList; + task.getAttributes (attrList); + + // Iterate over each task, and apply selection criteria. + for (unsigned int i = 0; i < all.size (); ++i) + { + T refTask (all[i]); + + // Apply description filter. + std::string desc = lowerCase (refTask.getDescription ()); + unsigned int matches = 0; + for (unsigned int w = 0; w < descWords.size (); ++w) + if (desc.find (descWords[w]) != std::string::npos) + ++matches; + + if (matches == descWords.size ()) + { + // Apply attribute filter. + matches = 0; + foreach (a, attrList) + if (a->first == "project") + { + if (a->second.length () <= refTask.getAttribute (a->first).length ()) + if (a->second == refTask.getAttribute (a->first).substr (0, a->second.length ())) + ++matches; + } + else if (a->second == refTask.getAttribute (a->first)) + ++matches; + + if (matches == attrList.size ()) + { + // Apply tag filter. + matches = 0; + for (unsigned int t = 0; t < tagList.size (); ++t) + if (refTask.hasTag (tagList[t])) + ++matches; + + if (matches == tagList.size ()) + filtered.push_back (refTask); + } + } + } + + all = filtered; +} + +//////////////////////////////////////////////////////////////////////////////// +// Successively apply filters based on the task object built from the command +// line. Tasks that match all the specified criteria are listed. +void handleList (const TDB& tdb, T& task, Config& conf) +{ + // Determine window size, and set table accordingly. + int width = conf.get ("defaultwidth", 80); +#ifdef HAVE_LIBNCURSES + if (conf.get ("curses", true)) + { + WINDOW* w = initscr (); + width = w->_maxx + 1; + endwin (); + } +#endif + + // Get the pending tasks. + tdb.gc (); + std::vector tasks; + tdb.allPendingT (tasks); + handleRecurrence (tdb, tasks); + filter (tasks, task); + + initializeColorRules (conf); + + bool showAge = conf.get ("showage", true); + + // Create a table for output. + Table table; + table.setTableWidth (width); + table.addColumn ("ID"); + table.addColumn ("Project"); + table.addColumn ("Pri"); + table.addColumn ("Due"); + table.addColumn ("Active"); + if (showAge) table.addColumn ("Age"); + table.addColumn ("Description"); + + if (conf.get ("color", true)) + { + table.setColumnUnderline (0); + table.setColumnUnderline (1); + table.setColumnUnderline (2); + table.setColumnUnderline (3); + table.setColumnUnderline (4); + table.setColumnUnderline (5); + if (showAge) table.setColumnUnderline (6); + } + + table.setColumnWidth (0, Table::minimum); + table.setColumnWidth (1, Table::minimum); + table.setColumnWidth (2, Table::minimum); + table.setColumnWidth (3, Table::minimum); + table.setColumnWidth (4, Table::minimum); + if (showAge) table.setColumnWidth (5, Table::minimum); + table.setColumnWidth ((showAge ? 6 : 5), Table::flexible); + + table.setColumnJustification (0, Table::right); + table.setColumnJustification (3, Table::right); + if (showAge) table.setColumnJustification (5, Table::right); + + table.sortOn (3, Table::ascendingDate); + table.sortOn (2, Table::descendingPriority); + table.sortOn (1, Table::ascendingCharacter); + + table.setDateFormat (conf.get ("dateformat", "m/d/Y")); + + for (unsigned int i = 0; i < tasks.size (); ++i) + { + T refTask (tasks[i]); + if (refTask.getStatus () != T::pending) + continue; + + // Now format the matching task. + bool imminent = false; + bool overdue = false; + std::string due = refTask.getAttribute ("due"); + if (due.length ()) + { + switch (getDueState (due)) + { + case 2: overdue = true; break; + case 1: imminent = true; break; + case 0: + default: break; + } + + Date dt (::atoi (due.c_str ())); + due = dt.toString (conf.get ("dateformat", "m/d/Y")); + } + + std::string active; + if (refTask.getAttribute ("start") != "") + active = "*"; + + std::string age; + std::string created = refTask.getAttribute ("entry"); + if (created.length ()) + { + Date now; + Date dt (::atoi (created.c_str ())); + formatTimeDeltaDays (age, (time_t) (now - dt)); + } + + // All criteria match, so add refTask to the output table. + int row = table.addRow (); + table.addCell (row, 0, refTask.getId ()); + table.addCell (row, 1, refTask.getAttribute ("project")); + table.addCell (row, 2, refTask.getAttribute ("priority")); + table.addCell (row, 3, due); + table.addCell (row, 4, active); + if (showAge) table.addCell (row, 5, age); + table.addCell (row, (showAge ? 6 : 5), refTask.getDescription ()); + + if (conf.get ("color", true)) + { + Text::color fg = Text::colorCode (refTask.getAttribute ("fg")); + Text::color bg = Text::colorCode (refTask.getAttribute ("bg")); + autoColorize (refTask, fg, bg); + table.setRowFg (row, fg); + table.setRowBg (row, bg); + + if (fg == Text::nocolor) + { + if (overdue) + table.setCellFg (row, 3, Text::red); + else if (imminent) + table.setCellFg (row, 3, Text::yellow); + } + } + } + + if (table.rowCount ()) + std::cout << optionalBlankLine (conf) + << table.render () + << optionalBlankLine (conf) + << table.rowCount () + << (table.rowCount () == 1 ? " task" : " tasks") + << std::endl; + else + std::cout << "No matches." + << std::endl; +} + +//////////////////////////////////////////////////////////////////////////////// +// Successively apply filters based on the task object built from the command +// line. Tasks that match all the specified criteria are listed. Show a narrow +// list that works better on mobile devices. +void handleSmallList (const TDB& tdb, T& task, Config& conf) +{ + // Determine window size, and set table accordingly. + int width = conf.get ("defaultwidth", 80); +#ifdef HAVE_LIBNCURSES + if (conf.get ("curses", true)) + { + WINDOW* w = initscr (); + width = w->_maxx + 1; + endwin (); + } +#endif + + // Get the pending tasks. + tdb.gc (); + std::vector tasks; + tdb.allPendingT (tasks); + handleRecurrence (tdb, tasks); + filter (tasks, task); + + initializeColorRules (conf); + + // Create a table for output. + Table table; + table.setTableWidth (width); + table.setDateFormat (conf.get ("dateformat", "m/d/Y")); + table.addColumn ("ID"); + table.addColumn ("Project"); + table.addColumn ("Pri"); + table.addColumn ("Description"); + + if (conf.get ("color", true)) + { + table.setColumnUnderline (0); + table.setColumnUnderline (1); + table.setColumnUnderline (2); + table.setColumnUnderline (3); + } + + table.setColumnWidth (0, Table::minimum); + table.setColumnWidth (1, Table::minimum); + table.setColumnWidth (2, Table::minimum); + table.setColumnWidth (3, Table::flexible); + + table.setColumnJustification (0, Table::right); + table.setColumnJustification (3, Table::left); + + table.sortOn (2, Table::descendingPriority); + table.sortOn (1, Table::ascendingCharacter); + + // Iterate over each task, and apply selection criteria. + for (unsigned int i = 0; i < tasks.size (); ++i) + { + T refTask (tasks[i]); + + // Now format the matching task. + bool imminent = false; + bool overdue = false; + std::string due = refTask.getAttribute ("due"); + if (due.length ()) + { + switch (getDueState (due)) + { + case 2: overdue = true; break; + case 1: imminent = true; break; + case 0: + default: break; + } + + Date dt (::atoi (due.c_str ())); + due = dt.toString (conf.get ("dateformat", "m/d/Y")); + } + + std::string active; + if (refTask.getAttribute ("start") != "") + active = "*"; + + std::string age; + std::string created = refTask.getAttribute ("entry"); + if (created.length ()) + { + Date now; + Date dt (::atoi (created.c_str ())); + formatTimeDeltaDays (age, (time_t) (now - dt)); + } + + // All criteria match, so add refTask to the output table. + int row = table.addRow (); + table.addCell (row, 0, refTask.getId ()); + table.addCell (row, 1, refTask.getAttribute ("project")); + table.addCell (row, 2, refTask.getAttribute ("priority")); + table.addCell (row, 3, refTask.getDescription ()); + + if (conf.get ("color", true)) + { + Text::color fg = Text::colorCode (refTask.getAttribute ("fg")); + Text::color bg = Text::colorCode (refTask.getAttribute ("bg")); + autoColorize (refTask, fg, bg); + table.setRowFg (row, fg); + table.setRowBg (row, bg); + + if (fg == Text::nocolor) + { + if (overdue) + table.setCellFg (row, 3, Text::red); + else if (imminent) + table.setCellFg (row, 3, Text::yellow); + } + } + } + + if (table.rowCount ()) + std::cout << optionalBlankLine (conf) + << table.render () + << optionalBlankLine (conf) + << table.rowCount () + << (table.rowCount () == 1 ? " task" : " tasks") + << std::endl; + else + std::cout << "No matches." + << std::endl; +} + +//////////////////////////////////////////////////////////////////////////////// +// Successively apply filters based on the task object built from the command +// line. Tasks that match all the specified criteria are listed. +void handleCompleted (const TDB& tdb, T& task, Config& conf) +{ + // Determine window size, and set table accordingly. + int width = conf.get ("defaultwidth", 80); +#ifdef HAVE_LIBNCURSES + if (conf.get ("curses", true)) + { + WINDOW* w = initscr (); + width = w->_maxx + 1; + endwin (); + } +#endif + + // Get the pending tasks. + tdb.gc (); + std::vector tasks; + tdb.completedT (tasks); + filter (tasks, task); + + initializeColorRules (conf); + + // Create a table for output. + Table table; + table.setTableWidth (width); + table.setDateFormat (conf.get ("dateformat", "m/d/Y")); + table.addColumn ("Done"); + table.addColumn ("Project"); + table.addColumn ("Description"); + + if (conf.get ("color", true)) + { + table.setColumnUnderline (0); + table.setColumnUnderline (1); + table.setColumnUnderline (2); + } + + table.setColumnWidth (0, Table::minimum); + table.setColumnWidth (1, Table::minimum); + table.setColumnWidth (2, Table::flexible); + + table.setColumnJustification (0, Table::right); + table.setColumnJustification (1, Table::left); + table.setColumnJustification (2, Table::left); + + table.sortOn (0, Table::ascendingDate); + + // Iterate over each task, and apply selection criteria. + for (unsigned int i = 0; i < tasks.size (); ++i) + { + T refTask (tasks[i]); + + // Now format the matching task. + Date end (::atoi (refTask.getAttribute ("end").c_str ())); + + // All criteria match, so add refTask to the output table. + int row = table.addRow (); + + table.addCell (row, 0, end.toString (conf.get ("dateformat", "m/d/Y"))); + table.addCell (row, 1, refTask.getAttribute ("project")); + table.addCell (row, 2, refTask.getDescription ()); + + if (conf.get ("color", true)) + { + Text::color fg = Text::colorCode (refTask.getAttribute ("fg")); + Text::color bg = Text::colorCode (refTask.getAttribute ("bg")); + autoColorize (refTask, fg, bg); + table.setRowFg (row, fg); + table.setRowBg (row, bg); + } + } + + if (table.rowCount ()) + std::cout << optionalBlankLine (conf) + << table.render () + << optionalBlankLine (conf) + << table.rowCount () + << (table.rowCount () == 1 ? " task" : " tasks") + << std::endl; + else + std::cout << "No matches." + << std::endl; +} + +//////////////////////////////////////////////////////////////////////////////// +// Display all information for the given task. +void handleInfo (const TDB& tdb, T& task, Config& conf) +{ + // Determine window size, and set table accordingly. + int width = conf.get ("defaultwidth", 80); +#ifdef HAVE_LIBNCURSES + if (conf.get ("curses", true)) + { + WINDOW* w = initscr (); + width = w->_maxx + 1; + endwin (); + } +#endif + + // Get all the tasks. + std::vector tasks; + tdb.allPendingT (tasks); + + Table table; + table.setTableWidth (width); + table.setDateFormat (conf.get ("dateformat", "m/d/Y")); + + table.addColumn ("Name"); + table.addColumn ("Value"); + + if (conf.get ("color", true)) + { + table.setColumnUnderline (0); + table.setColumnUnderline (1); + } + + table.setColumnWidth (0, Table::minimum); + table.setColumnWidth (1, Table::minimum); + + table.setColumnJustification (0, Table::left); + table.setColumnJustification (1, Table::left); + + // Find the task. + for (unsigned int i = 0; i < tasks.size (); ++i) + { + T refTask (tasks[i]); + + if (refTask.getId () == task.getId ()) + { + Date now; + + int row = table.addRow (); + table.addCell (row, 0, "ID"); + table.addCell (row, 1, refTask.getId ()); + + row = table.addRow (); + table.addCell (row, 0, "Status"); + table.addCell (row, 1, ( refTask.getStatus () == T::pending ? "Pending" + : refTask.getStatus () == T::completed ? "Completed" + : refTask.getStatus () == T::deleted ? "Deleted" + : refTask.getStatus () == T::recurring ? "Recurring" + : "")); + + row = table.addRow (); + table.addCell (row, 0, "Description"); + table.addCell (row, 1, refTask.getDescription ()); + + if (refTask.getAttribute ("project") != "") + { + row = table.addRow (); + table.addCell (row, 0, "Project"); + table.addCell (row, 1, refTask.getAttribute ("project")); + } + + if (refTask.getAttribute ("priority") != "") + { + row = table.addRow (); + table.addCell (row, 0, "Priority"); + table.addCell (row, 1, refTask.getAttribute ("priority")); + } + + if (refTask.getStatus () == T::recurring) + { + row = table.addRow (); + table.addCell (row, 0, "Recurrence"); + table.addCell (row, 1, refTask.getAttribute ("recur")); + + row = table.addRow (); + table.addCell (row, 0, "Recur until"); + table.addCell (row, 1, refTask.getAttribute ("until")); + } + + // due (colored) + bool imminent = false; + bool overdue = false; + std::string due = refTask.getAttribute ("due"); + if (due != "") + { + row = table.addRow (); + table.addCell (row, 0, "Due"); + + Date dt (::atoi (due.c_str ())); + due = dt.toString (conf.get ("dateformat", "m/d/Y")); + table.addCell (row, 1, due); + + if (due.length ()) + { + overdue = (dt < now) ? true : false; + Date nextweek = now + 7 * 86400; + imminent = dt < nextweek ? true : false; + + if (conf.get ("color", true)) + { + if (overdue) + table.setCellFg (row, 1, Text::red); + else if (imminent) + table.setCellFg (row, 1, Text::yellow); + } + } + } + + // start + if (refTask.getAttribute ("start") != "") + { + row = table.addRow (); + table.addCell (row, 0, "Start"); + Date dt (::atoi (refTask.getAttribute ("start").c_str ())); + table.addCell (row, 1, dt.toString (conf.get ("dateformat", "m/d/Y"))); + } + + // end + if (refTask.getAttribute ("end") != "") + { + row = table.addRow (); + table.addCell (row, 0, "End"); + Date dt (::atoi (refTask.getAttribute ("end").c_str ())); + table.addCell (row, 1, dt.toString (conf.get ("dateformat", "m/d/Y"))); + } + + // tags ... + std::vector tags; + refTask.getTags (tags); + if (tags.size ()) + { + std::string allTags; + join (allTags, " ", tags); + + row = table.addRow (); + table.addCell (row, 0, "Tags"); + table.addCell (row, 1, allTags); + } + + row = table.addRow (); + table.addCell (row, 0, "UUID"); + table.addCell (row, 1, refTask.getUUID ()); + + row = table.addRow (); + table.addCell (row, 0, "Entered"); + Date dt (::atoi (refTask.getAttribute ("entry").c_str ())); + std::string entry = dt.toString (conf.get ("dateformat", "m/d/Y")); + + std::string age; + std::string created = refTask.getAttribute ("entry"); + if (created.length ()) + { + Date dt (::atoi (created.c_str ())); + formatTimeDeltaDays (age, (time_t) (now - dt)); + } + + table.addCell (row, 1, entry + " (" + age + ")"); + } + } + + if (table.rowCount ()) + std::cout << optionalBlankLine (conf) + << table.render () + << optionalBlankLine (conf) + << table.rowCount () + << (table.rowCount () == 1 ? " task" : " tasks") + << std::endl; + else + std::cout << "No matches." << std::endl; +} + +//////////////////////////////////////////////////////////////////////////////// +// Successively apply filters based on the task object built from the command +// line. Tasks that match all the specified criteria are listed. +void handleLongList (const TDB& tdb, T& task, Config& conf) +{ + // Determine window size, and set table accordingly. + int width = conf.get ("defaultwidth", 80); +#ifdef HAVE_LIBNCURSES + if (conf.get ("curses", true)) + { + WINDOW* w = initscr (); + width = w->_maxx + 1; + endwin (); + } +#endif + + // Get all the tasks. + tdb.gc (); + std::vector tasks; + tdb.allPendingT (tasks); + handleRecurrence (tdb, tasks); + filter (tasks, task); + + initializeColorRules (conf); + + bool showAge = conf.get ("showage", true); + + // Create a table for output. + Table table; + table.setTableWidth (width); + table.setDateFormat (conf.get ("dateformat", "m/d/Y")); + table.addColumn ("ID"); + table.addColumn ("Project"); + table.addColumn ("Pri"); + table.addColumn ("Entry"); + table.addColumn ("Start"); + table.addColumn ("Due"); + if (showAge) table.addColumn ("Age"); + table.addColumn ("Tags"); + table.addColumn ("Description"); + + if (conf.get ("color", true)) + { + table.setColumnUnderline (0); + table.setColumnUnderline (1); + table.setColumnUnderline (2); + table.setColumnUnderline (3); + table.setColumnUnderline (4); + table.setColumnUnderline (5); + table.setColumnUnderline (6); + table.setColumnUnderline (7); + if (showAge) table.setColumnUnderline (8); + } + + table.setColumnWidth (0, Table::minimum); + table.setColumnWidth (1, Table::minimum); + table.setColumnWidth (2, Table::minimum); + table.setColumnWidth (3, Table::minimum); + table.setColumnWidth (4, Table::minimum); + table.setColumnWidth (5, Table::minimum); + if (showAge) table.setColumnWidth (6, Table::minimum); + table.setColumnWidth ((showAge ? 7 : 6), Table::minimum); + table.setColumnWidth ((showAge ? 8 : 7), Table::flexible); + + table.setColumnJustification (0, Table::right); + table.setColumnJustification (3, Table::right); + table.setColumnJustification (4, Table::right); + table.setColumnJustification (5, Table::right); + if (showAge) table.setColumnJustification (6, Table::right); + + table.sortOn (5, Table::ascendingDate); + table.sortOn (2, Table::descendingPriority); + table.sortOn (1, Table::ascendingCharacter); + + // Iterate over each task, and apply selection criteria. + for (unsigned int i = 0; i < tasks.size (); ++i) + { + T refTask (tasks[i]); + + Date now; + + std::string started = refTask.getAttribute ("start"); + if (started.length ()) + { + Date dt (::atoi (started.c_str ())); + started = dt.toString (conf.get ("dateformat", "m/d/Y")); + } + + std::string entered = refTask.getAttribute ("entry"); + if (entered.length ()) + { + Date dt (::atoi (entered.c_str ())); + entered = dt.toString (conf.get ("dateformat", "m/d/Y")); + } + + // Now format the matching task. + bool imminent = false; + bool overdue = false; + std::string due = refTask.getAttribute ("due"); + if (due.length ()) + { + switch (getDueState (due)) + { + case 2: overdue = true; break; + case 1: imminent = true; break; + case 0: + default: break; + } + + Date dt (::atoi (due.c_str ())); + due = dt.toString (conf.get ("dateformat", "m/d/Y")); + } + + std::string age; + std::string created = refTask.getAttribute ("entry"); + if (created.length ()) + { + Date dt (::atoi (created.c_str ())); + formatTimeDeltaDays (age, (time_t) (now - dt)); + } + + // Make a list of tags. + std::string tags; + std::vector all; + refTask.getTags (all); + join (tags, " ", all); + + // All criteria match, so add refTask to the output table. + int row = table.addRow (); + table.addCell (row, 0, refTask.getId ()); + table.addCell (row, 1, refTask.getAttribute ("project")); + table.addCell (row, 2, refTask.getAttribute ("priority")); + table.addCell (row, 3, entered); + table.addCell (row, 4, started); + table.addCell (row, 5, due); + if (showAge) table.addCell (row, 6, age); + table.addCell (row, (showAge ? 7 : 6), tags); + table.addCell (row, (showAge ? 8 : 7), refTask.getDescription ()); + + if (conf.get ("color", true)) + { + Text::color fg = Text::colorCode (refTask.getAttribute ("fg")); + Text::color bg = Text::colorCode (refTask.getAttribute ("bg")); + autoColorize (refTask, fg, bg); + table.setRowFg (row, fg); + table.setRowBg (row, bg); + + if (fg == Text::nocolor) + { + if (overdue) + table.setCellFg (row, 3, Text::red); + else if (imminent) + table.setCellFg (row, 3, Text::yellow); + } + } + } + + if (table.rowCount ()) + std::cout << optionalBlankLine (conf) + << table.render () + << optionalBlankLine (conf) + << table.rowCount () + << (table.rowCount () == 1 ? " task" : " tasks") + << std::endl; + else + std::cout << "No matches." << std::endl; +} + +//////////////////////////////////////////////////////////////////////////////// +// Project Tasks Avg Age Status +// A 12 13d XXXXXXXX------ +// B 109 3d 12h XX------------ +void handleReportSummary (const TDB& tdb, T& task, Config& conf) +{ + // Generate unique list of project names. + tdb.gc (); + std::map allProjects; + std::vector pending; + tdb.allPendingT (pending); + handleRecurrence (tdb, pending); + filter (pending, task); + for (unsigned int i = 0; i < pending.size (); ++i) + { + T task (pending[i]); + allProjects[task.getAttribute ("project")] = false; + } + + std::vector completed; + tdb.allCompletedT (completed); + filter (completed, task); + for (unsigned int i = 0; i < completed.size (); ++i) + { + T task (completed[i]); + allProjects[task.getAttribute ("project")] = false; + } + + // Initialize counts, sum. + std::map countPending; + std::map countCompleted; + std::map sumEntry; + std::map counter; + time_t now = time (NULL); + + foreach (i, allProjects) + { + countPending [i->first] = 0; + countCompleted [i->first] = 0; + sumEntry [i->first] = 0.0; + counter [i->first] = 0; + } + + // Count the pending tasks. + for (unsigned int i = 0; i < pending.size (); ++i) + { + T task (pending[i]); + std::string project = task.getAttribute ("project"); + if (task.getStatus () == T::pending) + ++countPending[project]; + + time_t entry = ::atoi (task.getAttribute ("entry").c_str ()); + if (entry) + { + sumEntry[project] = sumEntry[project] + (double) (now - entry); + ++counter[project]; + } + } + + // Count the completed tasks. + for (unsigned int i = 0; i < completed.size (); ++i) + { + T task (completed[i]); + std::string project = task.getAttribute ("project"); + countCompleted[project] = countCompleted[project] + 1; + ++counter[project]; + + time_t entry = ::atoi (task.getAttribute ("entry").c_str ()); + time_t end = ::atoi (task.getAttribute ("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%"); + + if (conf.get ("color", true)) + { + table.setColumnUnderline (0); + table.setColumnUnderline (1); + table.setColumnUnderline (2); + table.setColumnUnderline (3); + } + + table.setColumnJustification (1, Table::right); + table.setColumnJustification (2, Table::right); + table.setColumnJustification (3, Table::right); + + table.sortOn (0, Table::ascendingCharacter); + table.setDateFormat (conf.get ("dateformat", "m/d/Y")); + + 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; + formatTimeDeltaDays (age, (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 (conf.get ("color", true)) + { + 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); + } + } + + if (table.rowCount ()) + std::cout << optionalBlankLine (conf) + << table.render () + << optionalBlankLine (conf) + << table.rowCount () + << (table.rowCount () == 1 ? " project" : " projects") + << std::endl; + else + std::cout << "No projects." << std::endl; +} + +//////////////////////////////////////////////////////////////////////////////// +// A summary of the most important pending tasks. +// +// For every project, pull important tasks to present as an 'immediate' task +// list. This hides the overwhelming quantity of other tasks. +// +// Present at most three tasks for every project. Select the tasks using +// these criteria: +// - due:< 1wk, pri:* +// - due:*, pri:H +// - pri:H +// - due:*, pri:M +// - pri:M +// - due:*, pri:L +// - pri:L +// - due:*, pri:* <-- everything else +// +// Make the "three" tasks a configurable number +// +void handleReportNext (const TDB& tdb, T& task, Config& conf) +{ + // Load all pending. + tdb.gc (); + std::vector pending; + tdb.allPendingT (pending); + handleRecurrence (tdb, pending); + filter (pending, task); + + // Restrict to matching subset. + std::vector matching; + gatherNextTasks (tdb, task, conf, pending, matching); + + // Determine window size, and set table accordingly. + int width = conf.get ("defaultwidth", 80); +#ifdef HAVE_LIBNCURSES + if (conf.get ("curses", true)) + { + WINDOW* w = initscr (); + width = w->_maxx + 1; + endwin (); + } +#endif + + tdb.gc (); + + // Get the pending tasks. + std::vector tasks; + tdb.pendingT (tasks); + filter (tasks, task); + + initializeColorRules (conf); + + bool showAge = conf.get ("showage", true); + + // Create a table for output. + Table table; + table.setTableWidth (width); + table.setDateFormat (conf.get ("dateformat", "m/d/Y")); + table.addColumn ("ID"); + table.addColumn ("Project"); + table.addColumn ("Pri"); + table.addColumn ("Due"); + table.addColumn ("Active"); + if (showAge) table.addColumn ("Age"); + table.addColumn ("Description"); + + if (conf.get ("color", true)) + { + table.setColumnUnderline (0); + table.setColumnUnderline (1); + table.setColumnUnderline (2); + table.setColumnUnderline (3); + table.setColumnUnderline (4); + table.setColumnUnderline (5); + if (showAge) table.setColumnUnderline (6); + } + + table.setColumnWidth (0, Table::minimum); + table.setColumnWidth (1, Table::minimum); + table.setColumnWidth (2, Table::minimum); + table.setColumnWidth (3, Table::minimum); + table.setColumnWidth (4, Table::minimum); + if (showAge) table.setColumnWidth (5, Table::minimum); + table.setColumnWidth ((showAge ? 6 : 5), Table::flexible); + + table.setColumnJustification (0, Table::right); + table.setColumnJustification (3, Table::right); + if (showAge) table.setColumnJustification (5, Table::right); + + table.sortOn (3, Table::ascendingDate); + table.sortOn (2, Table::descendingPriority); + table.sortOn (1, Table::ascendingCharacter); + + // Iterate over each task, and apply selection criteria. + foreach (i, matching) + { + T refTask (pending[*i]); + Date now; + + // Now format the matching task. + bool imminent = false; + bool overdue = false; + std::string due = refTask.getAttribute ("due"); + if (due.length ()) + { + switch (getDueState (due)) + { + case 2: overdue = true; break; + case 1: imminent = true; break; + case 0: + default: break; + } + + Date dt (::atoi (due.c_str ())); + due = dt.toString (conf.get ("dateformat", "m/d/Y")); + } + + std::string active; + if (refTask.getAttribute ("start") != "") + active = "*"; + + std::string age; + std::string created = refTask.getAttribute ("entry"); + if (created.length ()) + { + Date dt (::atoi (created.c_str ())); + formatTimeDeltaDays (age, (time_t) (now - dt)); + } + + // All criteria match, so add refTask to the output table. + int row = table.addRow (); + table.addCell (row, 0, refTask.getId ()); + table.addCell (row, 1, refTask.getAttribute ("project")); + table.addCell (row, 2, refTask.getAttribute ("priority")); + table.addCell (row, 3, due); + table.addCell (row, 4, active); + if (showAge) table.addCell (row, 5, age); + table.addCell (row, (showAge ? 6 : 5), refTask.getDescription ()); + + if (conf.get ("color", true)) + { + Text::color fg = Text::colorCode (refTask.getAttribute ("fg")); + Text::color bg = Text::colorCode (refTask.getAttribute ("bg")); + autoColorize (refTask, fg, bg); + table.setRowFg (row, fg); + table.setRowBg (row, bg); + + if (fg == Text::nocolor) + { + if (overdue) + table.setCellFg (row, 3, Text::red); + else if (imminent) + table.setCellFg (row, 3, Text::yellow); + } + } + } + + if (table.rowCount ()) + std::cout << optionalBlankLine (conf) + << table.render () + << optionalBlankLine (conf) + << table.rowCount () + << (table.rowCount () == 1 ? " task" : " tasks") + << std::endl; + else + std::cout << "No matches." + << std::endl; +} + +//////////////////////////////////////////////////////////////////////////////// +// Year Month Added Completed Deleted +// 2006 November 87 63 14 +// 2006 December 21 6 1 +time_t monthlyEpoch (const std::string& date) +{ + // Convert any date in epoch form to m/d/y, then convert back + // to epoch form for the date m/1/y. + if (date.length ()) + { + Date d1 (::atoi (date.c_str ())); + int m, d, y; + d1.toMDY (m, d, y); + Date d2 (m, 1, y); + time_t epoch; + d2.toEpoch (epoch); + return epoch; + } + + return 0; +} + +void handleReportHistory (const TDB& tdb, T& task, Config& conf) +{ + std::map groups; + std::map addedGroup; + std::map completedGroup; + std::map deletedGroup; + + // Scan the pending tasks. + tdb.gc (); + std::vector pending; + tdb.allPendingT (pending); + handleRecurrence (tdb, pending); + filter (pending, task); + for (unsigned int i = 0; i < pending.size (); ++i) + { + T task (pending[i]); + time_t epoch = monthlyEpoch (task.getAttribute ("entry")); + if (epoch) + { + groups[epoch] = 0; + + if (addedGroup.find (epoch) != addedGroup.end ()) + addedGroup[epoch] = addedGroup[epoch] + 1; + else + addedGroup[epoch] = 1; + + if (task.getStatus () == T::deleted) + { + epoch = monthlyEpoch (task.getAttribute ("end")); + + if (deletedGroup.find (epoch) != deletedGroup.end ()) + deletedGroup[epoch] = deletedGroup[epoch] + 1; + else + deletedGroup[epoch] = 1; + } + else if (task.getStatus () == T::completed) + { + epoch = monthlyEpoch (task.getAttribute ("end")); + + if (completedGroup.find (epoch) != completedGroup.end ()) + completedGroup[epoch] = completedGroup[epoch] + 1; + else + completedGroup[epoch] = 1; + } + } + } + + // Scan the completed tasks. + std::vector completed; + tdb.allCompletedT (completed); + filter (completed, task); + for (unsigned int i = 0; i < completed.size (); ++i) + { + T task (completed[i]); + time_t epoch = monthlyEpoch (task.getAttribute ("entry")); + if (epoch) + { + groups[epoch] = 0; + + if (addedGroup.find (epoch) != addedGroup.end ()) + addedGroup[epoch] = addedGroup[epoch] + 1; + else + addedGroup[epoch] = 1; + + epoch = monthlyEpoch (task.getAttribute ("end")); + if (task.getStatus () == T::deleted) + { + epoch = monthlyEpoch (task.getAttribute ("end")); + + if (deletedGroup.find (epoch) != deletedGroup.end ()) + deletedGroup[epoch] = deletedGroup[epoch] + 1; + else + deletedGroup[epoch] = 1; + } + else if (task.getStatus () == T::completed) + { + epoch = monthlyEpoch (task.getAttribute ("end")); + if (completedGroup.find (epoch) != completedGroup.end ()) + completedGroup[epoch] = completedGroup[epoch] + 1; + else + completedGroup[epoch] = 1; + } + } + } + + // Now build the table. + Table table; + table.setDateFormat (conf.get ("dateformat", "m/d/Y")); + table.addColumn ("Year"); + table.addColumn ("Month"); + table.addColumn ("Added"); + table.addColumn ("Completed"); + table.addColumn ("Deleted"); + table.addColumn ("Net"); + + if (conf.get ("color", true)) + { + table.setColumnUnderline (0); + table.setColumnUnderline (1); + table.setColumnUnderline (2); + table.setColumnUnderline (3); + table.setColumnUnderline (4); + table.setColumnUnderline (5); + } + + 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 (conf.get ("color", true) && net) + table.setCellFg (row, 5, net > 0 ? Text::red: Text::green); + } + + if (table.rowCount ()) + { + table.addRow (); + row = table.addRow (); + + table.addCell (row, 1, "Average"); + if (conf.get ("color", true)) table.setRowFg (row, Text::bold); + 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)); + } + + if (table.rowCount ()) + std::cout << optionalBlankLine (conf) + << table.render () + << std::endl; + else + std::cout << "No tasks." << std::endl; +} + +//////////////////////////////////////////////////////////////////////////////// +void handleReportGHistory (const TDB& tdb, T& task, Config& conf) +{ + // Determine window size, and set table accordingly. + int width = conf.get ("defaultwidth", 80); +#ifdef HAVE_LIBNCURSES + if (conf.get ("curses", true)) + { + WINDOW* w = initscr (); + width = w->_maxx + 1; + endwin (); + } +#endif + int widthOfBar = width - 15; // strlen ("2008 September ") + + std::map groups; + std::map addedGroup; + std::map completedGroup; + std::map deletedGroup; + + // Scan the pending tasks. + tdb.gc (); + std::vector pending; + tdb.allPendingT (pending); + handleRecurrence (tdb, pending); + filter (pending, task); + for (unsigned int i = 0; i < pending.size (); ++i) + { + T task (pending[i]); + time_t epoch = monthlyEpoch (task.getAttribute ("entry")); + if (epoch) + { + groups[epoch] = 0; + + if (addedGroup.find (epoch) != addedGroup.end ()) + addedGroup[epoch] = addedGroup[epoch] + 1; + else + addedGroup[epoch] = 1; + + if (task.getStatus () == T::deleted) + { + epoch = monthlyEpoch (task.getAttribute ("end")); + + if (deletedGroup.find (epoch) != deletedGroup.end ()) + deletedGroup[epoch] = deletedGroup[epoch] + 1; + else + deletedGroup[epoch] = 1; + } + else if (task.getStatus () == T::completed) + { + epoch = monthlyEpoch (task.getAttribute ("end")); + + if (completedGroup.find (epoch) != completedGroup.end ()) + completedGroup[epoch] = completedGroup[epoch] + 1; + else + completedGroup[epoch] = 1; + } + } + } + + // Scan the completed tasks. + std::vector completed; + tdb.allCompletedT (completed); + filter (completed, task); + for (unsigned int i = 0; i < completed.size (); ++i) + { + T task (completed[i]); + time_t epoch = monthlyEpoch (task.getAttribute ("entry")); + if (epoch) + { + groups[epoch] = 0; + + if (addedGroup.find (epoch) != addedGroup.end ()) + addedGroup[epoch] = addedGroup[epoch] + 1; + else + addedGroup[epoch] = 1; + + epoch = monthlyEpoch (task.getAttribute ("end")); + if (task.getStatus () == T::deleted) + { + epoch = monthlyEpoch (task.getAttribute ("end")); + + if (deletedGroup.find (epoch) != deletedGroup.end ()) + deletedGroup[epoch] = deletedGroup[epoch] + 1; + else + deletedGroup[epoch] = 1; + } + else if (task.getStatus () == T::completed) + { + epoch = monthlyEpoch (task.getAttribute ("end")); + if (completedGroup.find (epoch) != completedGroup.end ()) + completedGroup[epoch] = completedGroup[epoch] + 1; + else + completedGroup[epoch] = 1; + } + } + } + + // Now build the table. + Table table; + table.setDateFormat (conf.get ("dateformat", "m/d/Y")); + table.addColumn ("Year"); + table.addColumn ("Month"); + table.addColumn ("Added/Completed/Deleted"); + + if (conf.get ("color", true)) + { + table.setColumnUnderline (0); + table.setColumnUnderline (1); + } + + // Determine the longest line. + int maxLine = 0; + foreach (i, groups) + { + int line = addedGroup[i->first] + completedGroup[i->first] + deletedGroup[i->first]; + + if (line > maxLine) + maxLine = line; + } + + if (maxLine > 0) + { + 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 (conf.get ("color", true)) + { + 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; + } + + bar = Text::colorize (Text::black, Text::on_red, aBar); + bar += Text::colorize (Text::black, Text::on_green, cBar); + bar += Text::colorize (Text::black, Text::on_yellow, 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 += "-"; + + bar = aBar + cBar + dBar; + } + + table.addCell (row, 2, bar); + } + } + + if (table.rowCount ()) + { + std::cout << optionalBlankLine (conf) + << table.render () + << std::endl; + + if (conf.get ("color", true)) + std::cout << "Legend: " + << Text::colorize (Text::black, Text::on_red, "added") + << ", " + << Text::colorize (Text::black, Text::on_green, "completed") + << ", " + << Text::colorize (Text::black, Text::on_yellow, "deleted") + << optionalBlankLine (conf) + << std::endl; + else + std::cout << "Legend: + added, X completed, - deleted" << std::endl; + } + else + std::cout << "No tasks." << std::endl; +} + +//////////////////////////////////////////////////////////////////////////////// +// A summary of the command usage. Not useful to users, but used to display +// usage statistics for feedback. +// +// 2006-12-04 19:59:43 "task list" +// +void handleReportUsage (const TDB& tdb, T& task, Config& conf) +{ + if (conf.get ("command.logging") == "on") + { + std::map usage; + std::vector all; + tdb.logRead (all); + for (unsigned int i = 0; i < all.size (); ++i) + { + // 0123456789012345678901 + // v 21 + // 2006-12-04 19:59:43 "task list" + std::string command = all[i].substr (21, all[i].length () - 22); + + // Parse as a command line. + std::vector args; + split (args, command, " "); + + try + { + T task; + std::string commandName; + parse (args, commandName, task, conf); + + usage[commandName]++; + } + + // Deliberately ignore errors from parsing the command log, as there may + // be commands from a prior version of task in there, which were + // abbreviated, and are now ambiguous. + catch (...) {} + } + + // Now render the table. + Table table; + table.addColumn ("Command"); + table.addColumn ("Frequency"); + + if (conf.get ("color", true)) + { + table.setColumnUnderline (0); + table.setColumnUnderline (1); + } + + table.setColumnJustification (1, Table::right); + table.sortOn (1, Table::descendingNumeric); + table.setDateFormat (conf.get ("dateformat", "m/d/Y")); + + foreach (i, usage) + { + int row = table.addRow (); + table.addCell (row, 0, (i->first == "" ? "(modify)" : i->first)); + table.addCell (row, 1, i->second); + } + + if (table.rowCount ()) + std::cout << optionalBlankLine (conf) + << table.render () + << std::endl; + else + std::cout << "No usage." << std::endl; + } + else + std::cout << "Command logging is not enabled, so no history has been kept." + << std::endl; +} + +//////////////////////////////////////////////////////////////////////////////// +std::string renderMonths ( + int firstMonth, + int firstYear, + const Date& today, + std::vector & all, + Config& conf) +{ + Table table; + table.setDateFormat (conf.get ("dateformat", "m/d/Y")); + int monthsPerLine = (conf.get ("monthsperline", 1)); + + // Build table for the number of months to be displayed. + for (int i = 0 ; i < (monthsPerLine * 8); i += 8) + { + table.addColumn (" "); + table.addColumn ("Su"); + table.addColumn ("Mo"); + table.addColumn ("Tu"); + table.addColumn ("We"); + table.addColumn ("Th"); + table.addColumn ("Fr"); + table.addColumn ("Sa"); + + if (conf.get ("color", true)) + { + table.setColumnUnderline (i + 1); + table.setColumnUnderline (i + 2); + table.setColumnUnderline (i + 3); + table.setColumnUnderline (i + 4); + table.setColumnUnderline (i + 5); + table.setColumnUnderline (i + 6); + table.setColumnUnderline (i + 7); + } + + table.setColumnJustification (i + 0, Table::right); + table.setColumnJustification (i + 1, Table::right); + table.setColumnJustification (i + 2, Table::right); + table.setColumnJustification (i + 3, Table::right); + table.setColumnJustification (i + 4, Table::right); + table.setColumnJustification (i + 5, Table::right); + table.setColumnJustification (i + 6, Table::right); + table.setColumnJustification (i + 7, Table::right); + } + + // At most, we need 6 rows. + table.addRow (); + table.addRow (); + table.addRow (); + table.addRow (); + table.addRow (); + table.addRow (); + + // Set number of days per month, months to render, and years to render. + std::vector years; + std::vector months; + std::vector daysInMonth; + int thisYear = firstYear; + int thisMonth = firstMonth; + for (int i = 0 ; i < monthsPerLine ; i++) + { + if (thisMonth < 13) + { + years.push_back (thisYear); + } + else + { + thisMonth -= 12; + years.push_back (++thisYear); + } + months.push_back (thisMonth); + daysInMonth.push_back (Date::daysInMonth (thisMonth++, thisYear)); + } + + int row = 0; + + // Loop through months to be added on this line. + for (int c = 0; c < monthsPerLine ; c++) + { + // Reset row counter for subsequent months + if (c != 0) + row = 0; + + // Loop through days in month and add to table. + for (int d = 1; d <= daysInMonth.at (c); ++d) + { + Date temp (months.at (c), d, years.at (c)); + int dow = temp.dayOfWeek (); + int thisCol = dow + 1 + (8 * c); + + table.addCell (row, thisCol, d); + + if (conf.get ("color", true) && + today.day () == d && + today.month () == months.at (c) && + today.year () == years.at (c)) + table.setCellFg (row, thisCol, Text::cyan); + + std::vector ::iterator it; + for (it = all.begin (); it != all.end (); ++it) + { + Date due (::atoi (it->getAttribute ("due").c_str ())); + + if (conf.get ("color", true) && + due.day () == d && + due.month () == months.at (c) && + due.year () == years.at (c)) + { + table.setCellFg (row, thisCol, Text::black); + table.setCellBg (row, thisCol, due < today ? Text::on_red : Text::on_yellow); + } + } + + // Check for end of week, and... + if (dow == 6 && d < daysInMonth.at (c)) + row++; + } + } + + return table.render (); +} + +//////////////////////////////////////////////////////////////////////////////// +void handleReportCalendar (const TDB& tdb, T& task, Config& conf) +{ + // Load all the pending tasks. + tdb.gc (); + std::vector pending; + tdb.allPendingT (pending); + handleRecurrence (tdb, pending); + filter (pending, task); + + // Find the oldest pending due date. + Date oldest; + Date newest; + std::vector ::iterator it; + for (it = pending.begin (); it != pending.end (); ++it) + { + if (it->getAttribute ("due") != "") + { + Date d (::atoi (it->getAttribute ("due").c_str ())); + + if (d < oldest) oldest = d; + if (d > newest) newest = d; + } + } + + // Iterate from oldest due month, year to newest month, year. + Date today; + int mFrom = oldest.month (); + int yFrom = oldest.year (); + + int mTo = newest.month (); + int yTo = newest.year (); + + std::cout << std::endl; + std::string output; + + int monthsPerLine = (conf.get ("monthsperline", 1)); + + 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++) + { + std::string month = Date::monthName (nextM); + std::cout << month + << " " + << std::setw(23 // one month's output width + - month.length ()// month name length + - 1)// spacer character + << std::left + << nextY; + + if (++nextM > 12) + { + nextM = 1; + nextY++; + } + } + + std::cout << std::endl + << optionalBlankLine (conf) + << renderMonths (mFrom, yFrom, today, pending, conf) + << std::endl; + + mFrom += monthsPerLine; + if (mFrom > 12) + { + mFrom -= 12; + ++yFrom; + } + } + + std::cout << "Legend: " + << Text::colorize (Text::cyan, Text::nocolor, "today") + << ", " + << Text::colorize (Text::black, Text::on_yellow, "due") + << ", " + << Text::colorize (Text::black, Text::on_red, "overdue") + << "." + << optionalBlankLine (conf) + << std::endl; +} + +//////////////////////////////////////////////////////////////////////////////// +void handleReportActive (const TDB& tdb, T& task, Config& conf) +{ + // Determine window size, and set table accordingly. + int width = conf.get ("defaultwidth", 80); +#ifdef HAVE_LIBNCURSES + if (conf.get ("curses", true)) + { + WINDOW* w = initscr (); + width = w->_maxx + 1; + endwin (); + } +#endif + + // Get all the tasks. + tdb.gc (); + std::vector tasks; + tdb.pendingT (tasks); + filter (tasks, task); + + initializeColorRules (conf); + + // Create a table for output. + Table table; + table.setTableWidth (width); + table.setDateFormat (conf.get ("dateformat", "m/d/Y")); + table.addColumn ("ID"); + table.addColumn ("Project"); + table.addColumn ("Pri"); + table.addColumn ("Due"); + table.addColumn ("Description"); + + if (conf.get ("color", true)) + { + table.setColumnUnderline (0); + table.setColumnUnderline (1); + table.setColumnUnderline (2); + table.setColumnUnderline (3); + table.setColumnUnderline (4); + } + + table.setColumnWidth (0, Table::minimum); + table.setColumnWidth (1, Table::minimum); + table.setColumnWidth (2, Table::minimum); + table.setColumnWidth (3, Table::minimum); + table.setColumnWidth (4, Table::flexible); + + table.setColumnJustification (0, Table::right); + table.setColumnJustification (3, Table::right); + + table.sortOn (3, Table::ascendingDate); + table.sortOn (2, Table::descendingPriority); + table.sortOn (1, Table::ascendingCharacter); + + // Iterate over each task, and apply selection criteria. + for (unsigned int i = 0; i < tasks.size (); ++i) + { + T refTask (tasks[i]); + if (refTask.getAttribute ("start") != "") + { + Date now; + bool imminent = false; + bool overdue = false; + std::string due = refTask.getAttribute ("due"); + if (due.length ()) + { + switch (getDueState (due)) + { + case 2: overdue = true; break; + case 1: imminent = true; break; + case 0: + default: break; + } + + Date dt (::atoi (due.c_str ())); + due = dt.toString (conf.get ("dateformat", "m/d/Y")); + } + + // All criteria match, so add refTask to the output table. + int row = table.addRow (); + table.addCell (row, 0, refTask.getId ()); + table.addCell (row, 1, refTask.getAttribute ("project")); + table.addCell (row, 2, refTask.getAttribute ("priority")); + table.addCell (row, 3, due); + table.addCell (row, 4, refTask.getDescription ()); + + if (conf.get ("color", true)) + { + Text::color fg = Text::colorCode (refTask.getAttribute ("fg")); + Text::color bg = Text::colorCode (refTask.getAttribute ("bg")); + autoColorize (refTask, fg, bg); + table.setRowFg (row, fg); + table.setRowBg (row, bg); + + if (fg == Text::nocolor) + { + if (overdue) + table.setCellFg (row, 3, Text::red); + else if (imminent) + table.setCellFg (row, 3, Text::yellow); + } + } + } + } + + if (table.rowCount ()) + std::cout << optionalBlankLine (conf) + << table.render () + << optionalBlankLine (conf) + << table.rowCount () + << (table.rowCount () == 1 ? " task" : " tasks") + << std::endl; + else + std::cout << "No active tasks." << std::endl; +} + +//////////////////////////////////////////////////////////////////////////////// +void handleReportOverdue (const TDB& tdb, T& task, Config& conf) +{ + // Determine window size, and set table accordingly. + int width = conf.get ("defaultwidth", 80); +#ifdef HAVE_LIBNCURSES + if (conf.get ("curses", true)) + { + WINDOW* w = initscr (); + width = w->_maxx + 1; + endwin (); + } +#endif + + // Get all the tasks. + std::vector tasks; + tdb.pendingT (tasks); + filter (tasks, task); + + initializeColorRules (conf); + + // Create a table for output. + Table table; + table.setTableWidth (width); + table.setDateFormat (conf.get ("dateformat", "m/d/Y")); + table.addColumn ("ID"); + table.addColumn ("Project"); + table.addColumn ("Pri"); + table.addColumn ("Due"); + table.addColumn ("Description"); + + if (conf.get ("color", true)) + { + table.setColumnUnderline (0); + table.setColumnUnderline (1); + table.setColumnUnderline (2); + table.setColumnUnderline (3); + table.setColumnUnderline (4); + } + + table.setColumnWidth (0, Table::minimum); + table.setColumnWidth (1, Table::minimum); + table.setColumnWidth (2, Table::minimum); + table.setColumnWidth (3, Table::minimum); + table.setColumnWidth (4, Table::flexible); + + table.setColumnJustification (0, Table::right); + table.setColumnJustification (3, Table::right); + + table.sortOn (3, Table::ascendingDate); + table.sortOn (2, Table::descendingPriority); + table.sortOn (1, Table::ascendingCharacter); + + Date now; + + // Iterate over each task, and apply selection criteria. + for (unsigned int i = 0; i < tasks.size (); ++i) + { + T refTask (tasks[i]); + std::string due; + if ((due = refTask.getAttribute ("due")) != "") + { + if (due.length ()) + { + Date dt (::atoi (due.c_str ())); + due = dt.toString (conf.get ("dateformat", "m/d/Y")); + + // If overdue. + if (dt < now) + { + // All criteria match, so add refTask to the output table. + int row = table.addRow (); + table.addCell (row, 0, refTask.getId ()); + table.addCell (row, 1, refTask.getAttribute ("project")); + table.addCell (row, 2, refTask.getAttribute ("priority")); + table.addCell (row, 3, due); + table.addCell (row, 4, refTask.getDescription ()); + + if (conf.get ("color", true)) + { + Text::color fg = Text::colorCode (refTask.getAttribute ("fg")); + Text::color bg = Text::colorCode (refTask.getAttribute ("bg")); + autoColorize (refTask, fg, bg); + table.setRowFg (row, fg); + table.setRowBg (row, bg); + + if (fg == Text::nocolor) + table.setCellFg (row, 3, Text::red); + } + } + } + } + } + + if (table.rowCount ()) + std::cout << optionalBlankLine (conf) + << table.render () + << optionalBlankLine (conf) + << table.rowCount () + << (table.rowCount () == 1 ? " task" : " tasks") + << std::endl; + else + std::cout << "No overdue tasks." << std::endl; +} + +//////////////////////////////////////////////////////////////////////////////// +// Successively apply filters based on the task object built from the command +// line. Tasks that match all the specified criteria are listed. +void handleReportOldest (const TDB& tdb, T& task, Config& conf) +{ + // Determine window size, and set table accordingly. + int width = conf.get ("defaultwidth", 80); +#ifdef HAVE_LIBNCURSES + if (conf.get ("curses", true)) + { + WINDOW* w = initscr (); + width = w->_maxx + 1; + endwin (); + } +#endif + + // Get the pending tasks. + tdb.gc (); + std::vector tasks; + tdb.allPendingT (tasks); + handleRecurrence (tdb, tasks); + filter (tasks, task); + + initializeColorRules (conf); + + bool showAge = conf.get ("showage", true); + unsigned int quantity = conf.get ("oldest", 10); + + // Create a table for output. + Table table; + table.setTableWidth (width); + table.addColumn ("ID"); + table.addColumn ("Project"); + table.addColumn ("Pri"); + table.addColumn ("Due"); + table.addColumn ("Active"); + if (showAge) table.addColumn ("Age"); + table.addColumn ("Description"); + + if (conf.get ("color", true)) + { + table.setColumnUnderline (0); + table.setColumnUnderline (1); + table.setColumnUnderline (2); + table.setColumnUnderline (3); + table.setColumnUnderline (4); + table.setColumnUnderline (5); + if (showAge) table.setColumnUnderline (6); + } + + table.setColumnWidth (0, Table::minimum); + table.setColumnWidth (1, Table::minimum); + table.setColumnWidth (2, Table::minimum); + table.setColumnWidth (3, Table::minimum); + table.setColumnWidth (4, Table::minimum); + if (showAge) table.setColumnWidth (5, Table::minimum); + table.setColumnWidth ((showAge ? 6 : 5), Table::flexible); + + table.setColumnJustification (0, Table::right); + table.setColumnJustification (3, Table::right); + if (showAge) table.setColumnJustification (5, Table::right); + + table.sortOn (3, Table::ascendingDate); + table.sortOn (2, Table::descendingPriority); + table.sortOn (1, Table::ascendingCharacter); + + table.setDateFormat (conf.get ("dateformat", "m/d/Y")); + + for (unsigned int i = 0; i < min (quantity, tasks.size ()); ++i) + { + T refTask (tasks[i]); + Date now; + + // Now format the matching task. + bool imminent = false; + bool overdue = false; + std::string due = refTask.getAttribute ("due"); + if (due.length ()) + { + switch (getDueState (due)) + { + case 2: overdue = true; break; + case 1: imminent = true; break; + case 0: + default: break; + } + + Date dt (::atoi (due.c_str ())); + due = dt.toString (conf.get ("dateformat", "m/d/Y")); + } + + std::string active; + if (refTask.getAttribute ("start") != "") + active = "*"; + + std::string age; + std::string created = refTask.getAttribute ("entry"); + if (created.length ()) + { + Date dt (::atoi (created.c_str ())); + formatTimeDeltaDays (age, (time_t) (now - dt)); + } + + // All criteria match, so add refTask to the output table. + int row = table.addRow (); + table.addCell (row, 0, refTask.getId ()); + table.addCell (row, 1, refTask.getAttribute ("project")); + table.addCell (row, 2, refTask.getAttribute ("priority")); + table.addCell (row, 3, due); + table.addCell (row, 4, active); + if (showAge) table.addCell (row, 5, age); + table.addCell (row, (showAge ? 6 : 5), refTask.getDescription ()); + + if (conf.get ("color", true)) + { + Text::color fg = Text::colorCode (refTask.getAttribute ("fg")); + Text::color bg = Text::colorCode (refTask.getAttribute ("bg")); + autoColorize (refTask, fg, bg); + table.setRowFg (row, fg); + table.setRowBg (row, bg); + + if (fg == Text::nocolor) + { + if (overdue) + table.setCellFg (row, 3, Text::red); + else if (imminent) + table.setCellFg (row, 3, Text::yellow); + } + } + } + + if (table.rowCount ()) + std::cout << optionalBlankLine (conf) + << table.render () + << optionalBlankLine (conf) + << table.rowCount () + << (table.rowCount () == 1 ? " task" : " tasks") + << std::endl; + else + std::cout << "No matches." + << std::endl; +} + +//////////////////////////////////////////////////////////////////////////////// +// Successively apply filters based on the task object built from the command +// line. Tasks that match all the specified criteria are listed. +void handleReportNewest (const TDB& tdb, T& task, Config& conf) +{ + // Determine window size, and set table accordingly. + int width = conf.get ("defaultwidth", 80); +#ifdef HAVE_LIBNCURSES + if (conf.get ("curses", true)) + { + WINDOW* w = initscr (); + width = w->_maxx + 1; + endwin (); + } +#endif + + // Get the pending tasks. + tdb.gc (); + std::vector tasks; + tdb.allPendingT (tasks); + handleRecurrence (tdb, tasks); + filter (tasks, task); + + initializeColorRules (conf); + + bool showAge = conf.get ("showage", true); + int quantity = conf.get ("newest", 10); + + // Create a table for output. + Table table; + table.setTableWidth (width); + table.addColumn ("ID"); + table.addColumn ("Project"); + table.addColumn ("Pri"); + table.addColumn ("Due"); + table.addColumn ("Active"); + if (showAge) table.addColumn ("Age"); + table.addColumn ("Description"); + + if (conf.get ("color", true)) + { + table.setColumnUnderline (0); + table.setColumnUnderline (1); + table.setColumnUnderline (2); + table.setColumnUnderline (3); + table.setColumnUnderline (4); + table.setColumnUnderline (5); + if (showAge) table.setColumnUnderline (6); + } + + table.setColumnWidth (0, Table::minimum); + table.setColumnWidth (1, Table::minimum); + table.setColumnWidth (2, Table::minimum); + table.setColumnWidth (3, Table::minimum); + table.setColumnWidth (4, Table::minimum); + if (showAge) table.setColumnWidth (5, Table::minimum); + table.setColumnWidth ((showAge ? 6 : 5), Table::flexible); + + table.setColumnJustification (0, Table::right); + table.setColumnJustification (3, Table::right); + if (showAge) table.setColumnJustification (5, Table::right); + + table.sortOn (3, Table::ascendingDate); + table.sortOn (2, Table::descendingPriority); + table.sortOn (1, Table::ascendingCharacter); + + table.setDateFormat (conf.get ("dateformat", "m/d/Y")); + + int total = tasks.size (); + for (int i = total - 1; i >= max (0, total - quantity); --i) + { + T refTask (tasks[i]); + Date now; + + // Now format the matching task. + bool imminent = false; + bool overdue = false; + std::string due = refTask.getAttribute ("due"); + if (due.length ()) + { + switch (getDueState (due)) + { + case 2: overdue = true; break; + case 1: imminent = true; break; + case 0: + default: break; + } + + Date dt (::atoi (due.c_str ())); + due = dt.toString (conf.get ("dateformat", "m/d/Y")); + } + + std::string active; + if (refTask.getAttribute ("start") != "") + active = "*"; + + std::string age; + std::string created = refTask.getAttribute ("entry"); + if (created.length ()) + { + Date dt (::atoi (created.c_str ())); + formatTimeDeltaDays (age, (time_t) (now - dt)); + } + + // All criteria match, so add refTask to the output table. + int row = table.addRow (); + table.addCell (row, 0, refTask.getId ()); + table.addCell (row, 1, refTask.getAttribute ("project")); + table.addCell (row, 2, refTask.getAttribute ("priority")); + table.addCell (row, 3, due); + table.addCell (row, 4, active); + if (showAge) table.addCell (row, 5, age); + table.addCell (row, (showAge ? 6 : 5), refTask.getDescription ()); + + if (conf.get ("color", true)) + { + Text::color fg = Text::colorCode (refTask.getAttribute ("fg")); + Text::color bg = Text::colorCode (refTask.getAttribute ("bg")); + autoColorize (refTask, fg, bg); + table.setRowFg (row, fg); + table.setRowBg (row, bg); + + if (fg == Text::nocolor) + { + if (overdue) + table.setCellFg (row, 3, Text::red); + else if (imminent) + table.setCellFg (row, 3, Text::yellow); + } + } + } + + if (table.rowCount ()) + std::cout << optionalBlankLine (conf) + << table.render () + << optionalBlankLine (conf) + << table.rowCount () + << (table.rowCount () == 1 ? " task" : " tasks") + << std::endl; + else + std::cout << "No matches." + << std::endl; +} + + +//////////////////////////////////////////////////////////////////////////////// +void handleReportStats (const TDB& tdb, T& task, Config& conf) +{ + // Get all the tasks. + std::vector tasks; + tdb.allT (tasks); + filter (tasks, task); + + Date now; + time_t earliest = time (NULL); + time_t latest = 1; + int totalT = 0; + int deletedT = 0; + int pendingT = 0; + int completedT = 0; + int taggedT = 0; + int recurringT = 0; + float daysPending = 0.0; + int descLength = 0; + + std::vector ::iterator it; + for (it = tasks.begin (); it != tasks.end (); ++it) + { + ++totalT; + if (it->getStatus () == T::deleted) ++deletedT; + if (it->getStatus () == T::pending) ++pendingT; + if (it->getStatus () == T::completed) ++completedT; + if (it->getStatus () == T::recurring) ++recurringT; + + time_t entry = ::atoi (it->getAttribute ("entry").c_str ()); + if (entry < earliest) earliest = entry; + if (entry > latest) latest = entry; + + if (it->getStatus () == T::completed) + { + time_t end = ::atoi (it->getAttribute ("end").c_str ()); + daysPending += (end - entry) / 86400.0; + } + + if (it->getStatus () == T::pending) + daysPending += (now - entry) / 86400.0; + + descLength += it->getDescription ().length (); + + std::vector tags; + it->getTags (tags); + if (tags.size ()) ++taggedT; + } + + std::cout << "Pending " << pendingT << std::endl + << "Recurring " << recurringT << std::endl + << "Completed " << completedT << std::endl + << "Deleted " << deletedT << std::endl + << "Total " << totalT << std::endl; + + if (tasks.size ()) + { + Date e (earliest); + std::cout << "Oldest task " << e.toString (conf.get ("dateformat", "m/d/Y")) << std::endl; + Date l (latest); + std::cout << "Newest task " << l.toString (conf.get ("dateformat", "m/d/Y")) << std::endl; + std::cout << "Task used for " << formatSeconds (latest - earliest) << std::endl; + } + + if (totalT) + std::cout << "Task added every " << formatSeconds ((latest - earliest) / totalT) << std::endl; + + if (completedT) + std::cout << "Task completed every " << formatSeconds ((latest - earliest) / completedT) << std::endl; + + if (deletedT) + std::cout << "Task deleted every " << formatSeconds ((latest - earliest) / deletedT) << std::endl; + + if (pendingT || completedT) + std::cout << "Average time pending " + << formatSeconds ((int) ((daysPending / (pendingT + completedT)) * 86400)) + << std::endl; + + if (totalT) + { + std::cout << "Average desc length " << (int) (descLength / totalT) << " characters" << std::endl; + std::cout << "Tasks tagged " << std::setprecision (3) << (100.0 * taggedT / totalT) << "%" << std::endl; + } +} + +//////////////////////////////////////////////////////////////////////////////// +void gatherNextTasks ( + const TDB& tdb, + T& task, + Config& conf, + std::vector & pending, + std::vector & all) +{ + // For counting tasks by project. + std::map countByProject; + std::map matching; + + Date now; + + // How many items per project? Default 3. + int limit = conf.get ("next", 3); + + // due:< 1wk, pri:* + for (unsigned int i = 0; i < pending.size (); ++i) + { + if (pending[i].getStatus () == T::pending) + { + std::string due = pending[i].getAttribute ("due"); + if (due != "") + { + Date d (::atoi (due.c_str ())); + if (d < now + (7 * 24 * 60 * 60)) // if due:< 1wk + { + std::string project = pending[i].getAttribute ("project"); + if (countByProject[project] < limit && matching.find (i) == matching.end ()) + { + ++countByProject[project]; + matching[i] = true; + } + } + } + } + } + + // due:*, pri:H + for (unsigned int i = 0; i < pending.size (); ++i) + { + if (pending[i].getStatus () == T::pending) + { + std::string due = pending[i].getAttribute ("due"); + if (due != "") + { + std::string priority = pending[i].getAttribute ("priority"); + if (priority == "H") + { + std::string project = pending[i].getAttribute ("project"); + if (countByProject[project] < limit && matching.find (i) == matching.end ()) + { + ++countByProject[project]; + matching[i] = true; + } + } + } + } + } + + // pri:H + for (unsigned int i = 0; i < pending.size (); ++i) + { + if (pending[i].getStatus () == T::pending) + { + std::string priority = pending[i].getAttribute ("priority"); + if (priority == "H") + { + std::string project = pending[i].getAttribute ("project"); + if (countByProject[project] < limit && matching.find (i) == matching.end ()) + { + ++countByProject[project]; + matching[i] = true; + } + } + } + } + + // due:*, pri:M + for (unsigned int i = 0; i < pending.size (); ++i) + { + if (pending[i].getStatus () == T::pending) + { + std::string due = pending[i].getAttribute ("due"); + if (due != "") + { + std::string priority = pending[i].getAttribute ("priority"); + if (priority == "M") + { + std::string project = pending[i].getAttribute ("project"); + if (countByProject[project] < limit && matching.find (i) == matching.end ()) + { + ++countByProject[project]; + matching[i] = true; + } + } + } + } + } + + // pri:M + for (unsigned int i = 0; i < pending.size (); ++i) + { + if (pending[i].getStatus () == T::pending) + { + std::string priority = pending[i].getAttribute ("priority"); + if (priority == "M") + { + std::string project = pending[i].getAttribute ("project"); + if (countByProject[project] < limit && matching.find (i) == matching.end ()) + { + ++countByProject[project]; + matching[i] = true; + } + } + } + } + + // due:*, pri:L + for (unsigned int i = 0; i < pending.size (); ++i) + { + if (pending[i].getStatus () == T::pending) + { + std::string due = pending[i].getAttribute ("due"); + if (due != "") + { + std::string priority = pending[i].getAttribute ("priority"); + if (priority == "L") + { + std::string project = pending[i].getAttribute ("project"); + if (countByProject[project] < limit && matching.find (i) == matching.end ()) + { + ++countByProject[project]; + matching[i] = true; + } + } + } + } + } + + // pri:L + for (unsigned int i = 0; i < pending.size (); ++i) + { + if (pending[i].getStatus () == T::pending) + { + std::string priority = pending[i].getAttribute ("priority"); + if (priority == "L") + { + std::string project = pending[i].getAttribute ("project"); + if (countByProject[project] < limit && matching.find (i) == matching.end ()) + { + ++countByProject[project]; + matching[i] = true; + } + } + } + } + + // due:, pri: + for (unsigned int i = 0; i < pending.size (); ++i) + { + if (pending[i].getStatus () == T::pending) + { + std::string due = pending[i].getAttribute ("due"); + if (due == "") + { + std::string priority = pending[i].getAttribute ("priority"); + if (priority == "") + { + std::string project = pending[i].getAttribute ("project"); + if (countByProject[project] < limit && matching.find (i) == matching.end ()) + { + ++countByProject[project]; + matching[i] = true; + } + } + } + } + } + + // Convert map to vector. + foreach (i, matching) + all.push_back (i->first); +} + +//////////////////////////////////////////////////////////////////////////////// diff --git a/src/task.cpp b/src/task.cpp index fe73dca1a..b324daa56 100644 --- a/src/task.cpp +++ b/src/task.cpp @@ -344,3113 +344,6 @@ int main (int argc, char** argv) return 0; } -//////////////////////////////////////////////////////////////////////////////// -static void filter (std::vector& all, T& task) -{ - std::vector filtered; - - // Split any description specified into words. - std::vector descWords; - split (descWords, lowerCase (task.getDescription ()), ' '); - - // Get all the tags to match against. - std::vector tagList; - task.getTags (tagList); - - // Get all the attributes to match against. - std::map attrList; - task.getAttributes (attrList); - - // Iterate over each task, and apply selection criteria. - for (unsigned int i = 0; i < all.size (); ++i) - { - T refTask (all[i]); - - // Apply description filter. - std::string desc = lowerCase (refTask.getDescription ()); - unsigned int matches = 0; - for (unsigned int w = 0; w < descWords.size (); ++w) - if (desc.find (descWords[w]) != std::string::npos) - ++matches; - - if (matches == descWords.size ()) - { - // Apply attribute filter. - matches = 0; - foreach (a, attrList) - if (a->first == "project") - { - if (a->second.length () <= refTask.getAttribute (a->first).length ()) - if (a->second == refTask.getAttribute (a->first).substr (0, a->second.length ())) - ++matches; - } - else if (a->second == refTask.getAttribute (a->first)) - ++matches; - - if (matches == attrList.size ()) - { - // Apply tag filter. - matches = 0; - for (unsigned int t = 0; t < tagList.size (); ++t) - if (refTask.hasTag (tagList[t])) - ++matches; - - if (matches == tagList.size ()) - filtered.push_back (refTask); - } - } - } - - all = filtered; -} - -//////////////////////////////////////////////////////////////////////////////// -void handleAdd (const TDB& tdb, T& task, Config& conf) -{ - char entryTime[16]; - sprintf (entryTime, "%u", (unsigned int) time (NULL)); - task.setAttribute ("entry", entryTime); - - std::map atts; - task.getAttributes (atts); - foreach (i, atts) - { - if (i->second == "") - task.removeAttribute (i->first); - } - - // Recurring tasks get a special status. - if (task.getAttribute ("due") != "" && - task.getAttribute ("recur") != "") - { - task.setStatus (T::recurring); - } - - if (task.getDescription () == "") - throw std::string ("Cannot add a blank task."); - - if (!tdb.addT (task)) - throw std::string ("Could not create new task."); -} - -//////////////////////////////////////////////////////////////////////////////// -void handleProjects (const TDB& tdb, T& task, Config& conf) -{ - // Get all the tasks, including deleted ones. - std::vector tasks; - tdb.pendingT (tasks); - - // Scan all the tasks for their project name, building a map using project - // names as keys. - std::map unique; - for (unsigned int i = 0; i < tasks.size (); ++i) - { - T task (tasks[i]); - unique[task.getAttribute ("project")] += 1; - } - - if (unique.size ()) - { - // Render a list of project names from the map. - Table table; - table.addColumn ("Project"); - table.addColumn ("Tasks"); - - if (conf.get ("color", true)) - { - table.setColumnUnderline (0); - table.setColumnUnderline (1); - } - - table.setColumnJustification (1, Table::right); - table.setDateFormat (conf.get ("dateformat", "m/d/Y")); - - foreach (i, unique) - { - int row = table.addRow (); - table.addCell (row, 0, i->first); - table.addCell (row, 1, i->second); - } - - std::cout << optionalBlankLine (conf) - << table.render () - << optionalBlankLine (conf) - << unique.size () - << (unique.size () == 1 ? " project" : " projects") - << std::endl; - } - else - std::cout << "No projects." - << std::endl; -} - -//////////////////////////////////////////////////////////////////////////////// -void handleTags (const TDB& tdb, T& task, Config& conf) -{ - // Get all the tasks. - std::vector tasks; - tdb.pendingT (tasks); - - // Scan all the tasks for their project name, building a map using project - // names as keys. - std::map unique; - for (unsigned int i = 0; i < tasks.size (); ++i) - { - T task (tasks[i]); - - std::vector tags; - task.getTags (tags); - - for (unsigned int t = 0; t < tags.size (); ++t) - unique[tags[t]] = ""; - } - - // Render a list of tag names from the map. - std::cout << optionalBlankLine (conf); - foreach (i, unique) - std::cout << i->first << std::endl; - - if (unique.size ()) - std::cout << optionalBlankLine (conf) - << unique.size () - << (unique.size () == 1 ? " tag" : " tags") - << std::endl; - else - std::cout << "No tags." - << std::endl; -} - -//////////////////////////////////////////////////////////////////////////////// -// Successively apply filters based on the task object built from the command -// line. Tasks that match all the specified criteria are listed. -void handleList (const TDB& tdb, T& task, Config& conf) -{ - // Determine window size, and set table accordingly. - int width = conf.get ("defaultwidth", 80); -#ifdef HAVE_LIBNCURSES - if (conf.get ("curses", true)) - { - WINDOW* w = initscr (); - width = w->_maxx + 1; - endwin (); - } -#endif - - // Get the pending tasks. - tdb.gc (); - std::vector tasks; - tdb.allPendingT (tasks); - handleRecurrence (tasks); - filter (tasks, task); - - initializeColorRules (conf); - - bool showAge = conf.get ("showage", true); - - // Create a table for output. - Table table; - table.setTableWidth (width); - table.addColumn ("ID"); - table.addColumn ("Project"); - table.addColumn ("Pri"); - table.addColumn ("Due"); - table.addColumn ("Active"); - if (showAge) table.addColumn ("Age"); - table.addColumn ("Description"); - - if (conf.get ("color", true)) - { - table.setColumnUnderline (0); - table.setColumnUnderline (1); - table.setColumnUnderline (2); - table.setColumnUnderline (3); - table.setColumnUnderline (4); - table.setColumnUnderline (5); - if (showAge) table.setColumnUnderline (6); - } - - table.setColumnWidth (0, Table::minimum); - table.setColumnWidth (1, Table::minimum); - table.setColumnWidth (2, Table::minimum); - table.setColumnWidth (3, Table::minimum); - table.setColumnWidth (4, Table::minimum); - if (showAge) table.setColumnWidth (5, Table::minimum); - table.setColumnWidth ((showAge ? 6 : 5), Table::flexible); - - table.setColumnJustification (0, Table::right); - table.setColumnJustification (3, Table::right); - if (showAge) table.setColumnJustification (5, Table::right); - - table.sortOn (3, Table::ascendingDate); - table.sortOn (2, Table::descendingPriority); - table.sortOn (1, Table::ascendingCharacter); - - table.setDateFormat (conf.get ("dateformat", "m/d/Y")); - - for (unsigned int i = 0; i < tasks.size (); ++i) - { - T refTask (tasks[i]); - if (refTask.getStatus () != T::pending) - continue; - - // Now format the matching task. - bool imminent = false; - bool overdue = false; - std::string due = refTask.getAttribute ("due"); - if (due.length ()) - { - switch (getDueState (due)) - { - case 2: overdue = true; break; - case 1: imminent = true; break; - case 0: - default: break; - } - - Date dt (::atoi (due.c_str ())); - due = dt.toString (conf.get ("dateformat", "m/d/Y")); - } - - std::string active; - if (refTask.getAttribute ("start") != "") - active = "*"; - - std::string age; - std::string created = refTask.getAttribute ("entry"); - if (created.length ()) - { - Date now; - Date dt (::atoi (created.c_str ())); - formatTimeDeltaDays (age, (time_t) (now - dt)); - } - - // All criteria match, so add refTask to the output table. - int row = table.addRow (); - table.addCell (row, 0, refTask.getId ()); - table.addCell (row, 1, refTask.getAttribute ("project")); - table.addCell (row, 2, refTask.getAttribute ("priority")); - table.addCell (row, 3, due); - table.addCell (row, 4, active); - if (showAge) table.addCell (row, 5, age); - table.addCell (row, (showAge ? 6 : 5), refTask.getDescription ()); - - if (conf.get ("color", true)) - { - Text::color fg = Text::colorCode (refTask.getAttribute ("fg")); - Text::color bg = Text::colorCode (refTask.getAttribute ("bg")); - autoColorize (refTask, fg, bg); - table.setRowFg (row, fg); - table.setRowBg (row, bg); - - if (fg == Text::nocolor) - { - if (overdue) - table.setCellFg (row, 3, Text::red); - else if (imminent) - table.setCellFg (row, 3, Text::yellow); - } - } - } - - if (table.rowCount ()) - std::cout << optionalBlankLine (conf) - << table.render () - << optionalBlankLine (conf) - << table.rowCount () - << (table.rowCount () == 1 ? " task" : " tasks") - << std::endl; - else - std::cout << "No matches." - << std::endl; -} - -//////////////////////////////////////////////////////////////////////////////// -// Successively apply filters based on the task object built from the command -// line. Tasks that match all the specified criteria are listed. Show a narrow -// list that works better on mobile devices. -void handleSmallList (const TDB& tdb, T& task, Config& conf) -{ - // Determine window size, and set table accordingly. - int width = conf.get ("defaultwidth", 80); -#ifdef HAVE_LIBNCURSES - if (conf.get ("curses", true)) - { - WINDOW* w = initscr (); - width = w->_maxx + 1; - endwin (); - } -#endif - - // Get the pending tasks. - tdb.gc (); - std::vector tasks; - tdb.allPendingT (tasks); - handleRecurrence (tasks); - filter (tasks, task); - - initializeColorRules (conf); - - // Create a table for output. - Table table; - table.setTableWidth (width); - table.setDateFormat (conf.get ("dateformat", "m/d/Y")); - table.addColumn ("ID"); - table.addColumn ("Project"); - table.addColumn ("Pri"); - table.addColumn ("Description"); - - if (conf.get ("color", true)) - { - table.setColumnUnderline (0); - table.setColumnUnderline (1); - table.setColumnUnderline (2); - table.setColumnUnderline (3); - } - - table.setColumnWidth (0, Table::minimum); - table.setColumnWidth (1, Table::minimum); - table.setColumnWidth (2, Table::minimum); - table.setColumnWidth (3, Table::flexible); - - table.setColumnJustification (0, Table::right); - table.setColumnJustification (3, Table::left); - - table.sortOn (2, Table::descendingPriority); - table.sortOn (1, Table::ascendingCharacter); - - // Iterate over each task, and apply selection criteria. - for (unsigned int i = 0; i < tasks.size (); ++i) - { - T refTask (tasks[i]); - - // Now format the matching task. - bool imminent = false; - bool overdue = false; - std::string due = refTask.getAttribute ("due"); - if (due.length ()) - { - switch (getDueState (due)) - { - case 2: overdue = true; break; - case 1: imminent = true; break; - case 0: - default: break; - } - - Date dt (::atoi (due.c_str ())); - due = dt.toString (conf.get ("dateformat", "m/d/Y")); - } - - std::string active; - if (refTask.getAttribute ("start") != "") - active = "*"; - - std::string age; - std::string created = refTask.getAttribute ("entry"); - if (created.length ()) - { - Date now; - Date dt (::atoi (created.c_str ())); - formatTimeDeltaDays (age, (time_t) (now - dt)); - } - - // All criteria match, so add refTask to the output table. - int row = table.addRow (); - table.addCell (row, 0, refTask.getId ()); - table.addCell (row, 1, refTask.getAttribute ("project")); - table.addCell (row, 2, refTask.getAttribute ("priority")); - table.addCell (row, 3, refTask.getDescription ()); - - if (conf.get ("color", true)) - { - Text::color fg = Text::colorCode (refTask.getAttribute ("fg")); - Text::color bg = Text::colorCode (refTask.getAttribute ("bg")); - autoColorize (refTask, fg, bg); - table.setRowFg (row, fg); - table.setRowBg (row, bg); - - if (fg == Text::nocolor) - { - if (overdue) - table.setCellFg (row, 3, Text::red); - else if (imminent) - table.setCellFg (row, 3, Text::yellow); - } - } - } - - if (table.rowCount ()) - std::cout << optionalBlankLine (conf) - << table.render () - << optionalBlankLine (conf) - << table.rowCount () - << (table.rowCount () == 1 ? " task" : " tasks") - << std::endl; - else - std::cout << "No matches." - << std::endl; -} - -//////////////////////////////////////////////////////////////////////////////// -// Successively apply filters based on the task object built from the command -// line. Tasks that match all the specified criteria are listed. -void handleCompleted (const TDB& tdb, T& task, Config& conf) -{ - // Determine window size, and set table accordingly. - int width = conf.get ("defaultwidth", 80); -#ifdef HAVE_LIBNCURSES - if (conf.get ("curses", true)) - { - WINDOW* w = initscr (); - width = w->_maxx + 1; - endwin (); - } -#endif - - // Get the pending tasks. - tdb.gc (); - std::vector tasks; - tdb.completedT (tasks); - filter (tasks, task); - - initializeColorRules (conf); - - // Create a table for output. - Table table; - table.setTableWidth (width); - table.setDateFormat (conf.get ("dateformat", "m/d/Y")); - table.addColumn ("Done"); - table.addColumn ("Project"); - table.addColumn ("Description"); - - if (conf.get ("color", true)) - { - table.setColumnUnderline (0); - table.setColumnUnderline (1); - table.setColumnUnderline (2); - } - - table.setColumnWidth (0, Table::minimum); - table.setColumnWidth (1, Table::minimum); - table.setColumnWidth (2, Table::flexible); - - table.setColumnJustification (0, Table::right); - table.setColumnJustification (1, Table::left); - table.setColumnJustification (2, Table::left); - - table.sortOn (0, Table::ascendingDate); - - // Iterate over each task, and apply selection criteria. - for (unsigned int i = 0; i < tasks.size (); ++i) - { - T refTask (tasks[i]); - - // Now format the matching task. - Date end (::atoi (refTask.getAttribute ("end").c_str ())); - - // All criteria match, so add refTask to the output table. - int row = table.addRow (); - - table.addCell (row, 0, end.toString (conf.get ("dateformat", "m/d/Y"))); - table.addCell (row, 1, refTask.getAttribute ("project")); - table.addCell (row, 2, refTask.getDescription ()); - - if (conf.get ("color", true)) - { - Text::color fg = Text::colorCode (refTask.getAttribute ("fg")); - Text::color bg = Text::colorCode (refTask.getAttribute ("bg")); - autoColorize (refTask, fg, bg); - table.setRowFg (row, fg); - table.setRowBg (row, bg); - } - } - - if (table.rowCount ()) - std::cout << optionalBlankLine (conf) - << table.render () - << optionalBlankLine (conf) - << table.rowCount () - << (table.rowCount () == 1 ? " task" : " tasks") - << std::endl; - else - std::cout << "No matches." - << std::endl; -} - -//////////////////////////////////////////////////////////////////////////////// -// Display all information for the given task. -void handleInfo (const TDB& tdb, T& task, Config& conf) -{ - // Determine window size, and set table accordingly. - int width = conf.get ("defaultwidth", 80); -#ifdef HAVE_LIBNCURSES - if (conf.get ("curses", true)) - { - WINDOW* w = initscr (); - width = w->_maxx + 1; - endwin (); - } -#endif - - // Get all the tasks. - std::vector tasks; - tdb.allPendingT (tasks); - - Table table; - table.setTableWidth (width); - table.setDateFormat (conf.get ("dateformat", "m/d/Y")); - - table.addColumn ("Name"); - table.addColumn ("Value"); - - if (conf.get ("color", true)) - { - table.setColumnUnderline (0); - table.setColumnUnderline (1); - } - - table.setColumnWidth (0, Table::minimum); - table.setColumnWidth (1, Table::minimum); - - table.setColumnJustification (0, Table::left); - table.setColumnJustification (1, Table::left); - - // Find the task. - for (unsigned int i = 0; i < tasks.size (); ++i) - { - T refTask (tasks[i]); - - if (refTask.getId () == task.getId ()) - { - Date now; - - int row = table.addRow (); - table.addCell (row, 0, "ID"); - table.addCell (row, 1, refTask.getId ()); - - row = table.addRow (); - table.addCell (row, 0, "Status"); - table.addCell (row, 1, ( refTask.getStatus () == T::pending ? "Pending" - : refTask.getStatus () == T::completed ? "Completed" - : refTask.getStatus () == T::deleted ? "Deleted" - : refTask.getStatus () == T::recurring ? "Recurring" - : "")); - - row = table.addRow (); - table.addCell (row, 0, "Description"); - table.addCell (row, 1, refTask.getDescription ()); - - if (refTask.getAttribute ("project") != "") - { - row = table.addRow (); - table.addCell (row, 0, "Project"); - table.addCell (row, 1, refTask.getAttribute ("project")); - } - - if (refTask.getAttribute ("priority") != "") - { - row = table.addRow (); - table.addCell (row, 0, "Priority"); - table.addCell (row, 1, refTask.getAttribute ("priority")); - } - - if (refTask.getStatus () == T::recurring) - { - row = table.addRow (); - table.addCell (row, 0, "Recurrence"); - table.addCell (row, 1, refTask.getAttribute ("recur")); - - row = table.addRow (); - table.addCell (row, 0, "Recur until"); - table.addCell (row, 1, refTask.getAttribute ("until")); - } - - // due (colored) - bool imminent = false; - bool overdue = false; - std::string due = refTask.getAttribute ("due"); - if (due != "") - { - row = table.addRow (); - table.addCell (row, 0, "Due"); - - Date dt (::atoi (due.c_str ())); - due = dt.toString (conf.get ("dateformat", "m/d/Y")); - table.addCell (row, 1, due); - - if (due.length ()) - { - overdue = (dt < now) ? true : false; - Date nextweek = now + 7 * 86400; - imminent = dt < nextweek ? true : false; - - if (conf.get ("color", true)) - { - if (overdue) - table.setCellFg (row, 1, Text::red); - else if (imminent) - table.setCellFg (row, 1, Text::yellow); - } - } - } - - // start - if (refTask.getAttribute ("start") != "") - { - row = table.addRow (); - table.addCell (row, 0, "Start"); - Date dt (::atoi (refTask.getAttribute ("start").c_str ())); - table.addCell (row, 1, dt.toString (conf.get ("dateformat", "m/d/Y"))); - } - - // end - if (refTask.getAttribute ("end") != "") - { - row = table.addRow (); - table.addCell (row, 0, "End"); - Date dt (::atoi (refTask.getAttribute ("end").c_str ())); - table.addCell (row, 1, dt.toString (conf.get ("dateformat", "m/d/Y"))); - } - - // tags ... - std::vector tags; - refTask.getTags (tags); - if (tags.size ()) - { - std::string allTags; - join (allTags, " ", tags); - - row = table.addRow (); - table.addCell (row, 0, "Tags"); - table.addCell (row, 1, allTags); - } - - row = table.addRow (); - table.addCell (row, 0, "UUID"); - table.addCell (row, 1, refTask.getUUID ()); - - row = table.addRow (); - table.addCell (row, 0, "Entered"); - Date dt (::atoi (refTask.getAttribute ("entry").c_str ())); - std::string entry = dt.toString (conf.get ("dateformat", "m/d/Y")); - - std::string age; - std::string created = refTask.getAttribute ("entry"); - if (created.length ()) - { - Date dt (::atoi (created.c_str ())); - formatTimeDeltaDays (age, (time_t) (now - dt)); - } - - table.addCell (row, 1, entry + " (" + age + ")"); - } - } - - if (table.rowCount ()) - std::cout << optionalBlankLine (conf) - << table.render () - << optionalBlankLine (conf) - << table.rowCount () - << (table.rowCount () == 1 ? " task" : " tasks") - << std::endl; - else - std::cout << "No matches." << std::endl; -} - -//////////////////////////////////////////////////////////////////////////////// -// If a task is deleted, but is still in the pending file, then it may be -// undeleted simply by changing it's status. -void handleUndelete (const TDB& tdb, T& task, Config& conf) -{ - std::vector all; - tdb.allPendingT (all); - - int id = task.getId (); - std::vector ::iterator it; - for (it = all.begin (); it != all.end (); ++it) - { - if (it->getId () == id) - { - if (it->getStatus () == T::deleted) - { - if (it->getAttribute ("recur") != "") - { - std::cout << "Task does not support 'undelete' for recurring tasks." << std::endl; - return; - } - - T restored (*it); - restored.setStatus (T::pending); - restored.removeAttribute ("end"); - tdb.modifyT (restored); - - std::cout << "Task " << id << " successfully undeleted." << std::endl; - return; - } - else - { - std::cout << "Task " << id << " is not deleted - therefore cannot undelete." << std::endl; - return; - } - } - } - - std::cout << "Task " << id - << " not found - tasks can only be reliably undeleted if the undelete" << std::endl - << "command is run immediately after the errant delete command." << std::endl; -} - -//////////////////////////////////////////////////////////////////////////////// -// Successively apply filters based on the task object built from the command -// line. Tasks that match all the specified criteria are listed. -void handleLongList (const TDB& tdb, T& task, Config& conf) -{ - // Determine window size, and set table accordingly. - int width = conf.get ("defaultwidth", 80); -#ifdef HAVE_LIBNCURSES - if (conf.get ("curses", true)) - { - WINDOW* w = initscr (); - width = w->_maxx + 1; - endwin (); - } -#endif - - // Get all the tasks. - tdb.gc (); - std::vector tasks; - tdb.allPendingT (tasks); - handleRecurrence (tasks); - filter (tasks, task); - - initializeColorRules (conf); - - bool showAge = conf.get ("showage", true); - - // Create a table for output. - Table table; - table.setTableWidth (width); - table.setDateFormat (conf.get ("dateformat", "m/d/Y")); - table.addColumn ("ID"); - table.addColumn ("Project"); - table.addColumn ("Pri"); - table.addColumn ("Entry"); - table.addColumn ("Start"); - table.addColumn ("Due"); - if (showAge) table.addColumn ("Age"); - table.addColumn ("Tags"); - table.addColumn ("Description"); - - if (conf.get ("color", true)) - { - table.setColumnUnderline (0); - table.setColumnUnderline (1); - table.setColumnUnderline (2); - table.setColumnUnderline (3); - table.setColumnUnderline (4); - table.setColumnUnderline (5); - table.setColumnUnderline (6); - table.setColumnUnderline (7); - if (showAge) table.setColumnUnderline (8); - } - - table.setColumnWidth (0, Table::minimum); - table.setColumnWidth (1, Table::minimum); - table.setColumnWidth (2, Table::minimum); - table.setColumnWidth (3, Table::minimum); - table.setColumnWidth (4, Table::minimum); - table.setColumnWidth (5, Table::minimum); - if (showAge) table.setColumnWidth (6, Table::minimum); - table.setColumnWidth ((showAge ? 7 : 6), Table::minimum); - table.setColumnWidth ((showAge ? 8 : 7), Table::flexible); - - table.setColumnJustification (0, Table::right); - table.setColumnJustification (3, Table::right); - table.setColumnJustification (4, Table::right); - table.setColumnJustification (5, Table::right); - if (showAge) table.setColumnJustification (6, Table::right); - - table.sortOn (5, Table::ascendingDate); - table.sortOn (2, Table::descendingPriority); - table.sortOn (1, Table::ascendingCharacter); - - // Iterate over each task, and apply selection criteria. - for (unsigned int i = 0; i < tasks.size (); ++i) - { - T refTask (tasks[i]); - - Date now; - - std::string started = refTask.getAttribute ("start"); - if (started.length ()) - { - Date dt (::atoi (started.c_str ())); - started = dt.toString (conf.get ("dateformat", "m/d/Y")); - } - - std::string entered = refTask.getAttribute ("entry"); - if (entered.length ()) - { - Date dt (::atoi (entered.c_str ())); - entered = dt.toString (conf.get ("dateformat", "m/d/Y")); - } - - // Now format the matching task. - bool imminent = false; - bool overdue = false; - std::string due = refTask.getAttribute ("due"); - if (due.length ()) - { - switch (getDueState (due)) - { - case 2: overdue = true; break; - case 1: imminent = true; break; - case 0: - default: break; - } - - Date dt (::atoi (due.c_str ())); - due = dt.toString (conf.get ("dateformat", "m/d/Y")); - } - - std::string age; - std::string created = refTask.getAttribute ("entry"); - if (created.length ()) - { - Date dt (::atoi (created.c_str ())); - formatTimeDeltaDays (age, (time_t) (now - dt)); - } - - // Make a list of tags. - std::string tags; - std::vector all; - refTask.getTags (all); - join (tags, " ", all); - - // All criteria match, so add refTask to the output table. - int row = table.addRow (); - table.addCell (row, 0, refTask.getId ()); - table.addCell (row, 1, refTask.getAttribute ("project")); - table.addCell (row, 2, refTask.getAttribute ("priority")); - table.addCell (row, 3, entered); - table.addCell (row, 4, started); - table.addCell (row, 5, due); - if (showAge) table.addCell (row, 6, age); - table.addCell (row, (showAge ? 7 : 6), tags); - table.addCell (row, (showAge ? 8 : 7), refTask.getDescription ()); - - if (conf.get ("color", true)) - { - Text::color fg = Text::colorCode (refTask.getAttribute ("fg")); - Text::color bg = Text::colorCode (refTask.getAttribute ("bg")); - autoColorize (refTask, fg, bg); - table.setRowFg (row, fg); - table.setRowBg (row, bg); - - if (fg == Text::nocolor) - { - if (overdue) - table.setCellFg (row, 3, Text::red); - else if (imminent) - table.setCellFg (row, 3, Text::yellow); - } - } - } - - if (table.rowCount ()) - std::cout << optionalBlankLine (conf) - << table.render () - << optionalBlankLine (conf) - << table.rowCount () - << (table.rowCount () == 1 ? " task" : " tasks") - << std::endl; - else - std::cout << "No matches." << std::endl; -} - -//////////////////////////////////////////////////////////////////////////////// -// Project Tasks Avg Age Status -// A 12 13d XXXXXXXX------ -// B 109 3d 12h XX------------ -void handleReportSummary (const TDB& tdb, T& task, Config& conf) -{ - // Generate unique list of project names. - tdb.gc (); - std::map allProjects; - std::vector pending; - tdb.allPendingT (pending); - handleRecurrence (pending); - filter (pending, task); - for (unsigned int i = 0; i < pending.size (); ++i) - { - T task (pending[i]); - allProjects[task.getAttribute ("project")] = false; - } - - std::vector completed; - tdb.allCompletedT (completed); - filter (completed, task); - for (unsigned int i = 0; i < completed.size (); ++i) - { - T task (completed[i]); - allProjects[task.getAttribute ("project")] = false; - } - - // Initialize counts, sum. - std::map countPending; - std::map countCompleted; - std::map sumEntry; - std::map counter; - time_t now = time (NULL); - - foreach (i, allProjects) - { - countPending [i->first] = 0; - countCompleted [i->first] = 0; - sumEntry [i->first] = 0.0; - counter [i->first] = 0; - } - - // Count the pending tasks. - for (unsigned int i = 0; i < pending.size (); ++i) - { - T task (pending[i]); - std::string project = task.getAttribute ("project"); - if (task.getStatus () == T::pending) - ++countPending[project]; - - time_t entry = ::atoi (task.getAttribute ("entry").c_str ()); - if (entry) - { - sumEntry[project] = sumEntry[project] + (double) (now - entry); - ++counter[project]; - } - } - - // Count the completed tasks. - for (unsigned int i = 0; i < completed.size (); ++i) - { - T task (completed[i]); - std::string project = task.getAttribute ("project"); - countCompleted[project] = countCompleted[project] + 1; - ++counter[project]; - - time_t entry = ::atoi (task.getAttribute ("entry").c_str ()); - time_t end = ::atoi (task.getAttribute ("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%"); - - if (conf.get ("color", true)) - { - table.setColumnUnderline (0); - table.setColumnUnderline (1); - table.setColumnUnderline (2); - table.setColumnUnderline (3); - } - - table.setColumnJustification (1, Table::right); - table.setColumnJustification (2, Table::right); - table.setColumnJustification (3, Table::right); - - table.sortOn (0, Table::ascendingCharacter); - table.setDateFormat (conf.get ("dateformat", "m/d/Y")); - - 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; - formatTimeDeltaDays (age, (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 (conf.get ("color", true)) - { - 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); - } - } - - if (table.rowCount ()) - std::cout << optionalBlankLine (conf) - << table.render () - << optionalBlankLine (conf) - << table.rowCount () - << (table.rowCount () == 1 ? " project" : " projects") - << std::endl; - else - std::cout << "No projects." << std::endl; -} - -//////////////////////////////////////////////////////////////////////////////// -// A summary of the most important pending tasks. -// -// For every project, pull important tasks to present as an 'immediate' task -// list. This hides the overwhelming quantity of other tasks. -// -// Present at most three tasks for every project. Select the tasks using -// these criteria: -// - due:< 1wk, pri:* -// - due:*, pri:H -// - pri:H -// - due:*, pri:M -// - pri:M -// - due:*, pri:L -// - pri:L -// - due:*, pri:* <-- everything else -// -// Make the "three" tasks a configurable number -// -void handleReportNext (const TDB& tdb, T& task, Config& conf) -{ - // Load all pending. - tdb.gc (); - std::vector pending; - tdb.allPendingT (pending); - handleRecurrence (pending); - filter (pending, task); - - // Restrict to matching subset. - std::vector matching; - gatherNextTasks (tdb, task, conf, pending, matching); - - // Determine window size, and set table accordingly. - int width = conf.get ("defaultwidth", 80); -#ifdef HAVE_LIBNCURSES - if (conf.get ("curses", true)) - { - WINDOW* w = initscr (); - width = w->_maxx + 1; - endwin (); - } -#endif - - tdb.gc (); - - // Get the pending tasks. - std::vector tasks; - tdb.pendingT (tasks); - filter (tasks, task); - - initializeColorRules (conf); - - bool showAge = conf.get ("showage", true); - - // Create a table for output. - Table table; - table.setTableWidth (width); - table.setDateFormat (conf.get ("dateformat", "m/d/Y")); - table.addColumn ("ID"); - table.addColumn ("Project"); - table.addColumn ("Pri"); - table.addColumn ("Due"); - table.addColumn ("Active"); - if (showAge) table.addColumn ("Age"); - table.addColumn ("Description"); - - if (conf.get ("color", true)) - { - table.setColumnUnderline (0); - table.setColumnUnderline (1); - table.setColumnUnderline (2); - table.setColumnUnderline (3); - table.setColumnUnderline (4); - table.setColumnUnderline (5); - if (showAge) table.setColumnUnderline (6); - } - - table.setColumnWidth (0, Table::minimum); - table.setColumnWidth (1, Table::minimum); - table.setColumnWidth (2, Table::minimum); - table.setColumnWidth (3, Table::minimum); - table.setColumnWidth (4, Table::minimum); - if (showAge) table.setColumnWidth (5, Table::minimum); - table.setColumnWidth ((showAge ? 6 : 5), Table::flexible); - - table.setColumnJustification (0, Table::right); - table.setColumnJustification (3, Table::right); - if (showAge) table.setColumnJustification (5, Table::right); - - table.sortOn (3, Table::ascendingDate); - table.sortOn (2, Table::descendingPriority); - table.sortOn (1, Table::ascendingCharacter); - - // Iterate over each task, and apply selection criteria. - foreach (i, matching) - { - T refTask (pending[*i]); - Date now; - - // Now format the matching task. - bool imminent = false; - bool overdue = false; - std::string due = refTask.getAttribute ("due"); - if (due.length ()) - { - switch (getDueState (due)) - { - case 2: overdue = true; break; - case 1: imminent = true; break; - case 0: - default: break; - } - - Date dt (::atoi (due.c_str ())); - due = dt.toString (conf.get ("dateformat", "m/d/Y")); - } - - std::string active; - if (refTask.getAttribute ("start") != "") - active = "*"; - - std::string age; - std::string created = refTask.getAttribute ("entry"); - if (created.length ()) - { - Date dt (::atoi (created.c_str ())); - formatTimeDeltaDays (age, (time_t) (now - dt)); - } - - // All criteria match, so add refTask to the output table. - int row = table.addRow (); - table.addCell (row, 0, refTask.getId ()); - table.addCell (row, 1, refTask.getAttribute ("project")); - table.addCell (row, 2, refTask.getAttribute ("priority")); - table.addCell (row, 3, due); - table.addCell (row, 4, active); - if (showAge) table.addCell (row, 5, age); - table.addCell (row, (showAge ? 6 : 5), refTask.getDescription ()); - - if (conf.get ("color", true)) - { - Text::color fg = Text::colorCode (refTask.getAttribute ("fg")); - Text::color bg = Text::colorCode (refTask.getAttribute ("bg")); - autoColorize (refTask, fg, bg); - table.setRowFg (row, fg); - table.setRowBg (row, bg); - - if (fg == Text::nocolor) - { - if (overdue) - table.setCellFg (row, 3, Text::red); - else if (imminent) - table.setCellFg (row, 3, Text::yellow); - } - } - } - - if (table.rowCount ()) - std::cout << optionalBlankLine (conf) - << table.render () - << optionalBlankLine (conf) - << table.rowCount () - << (table.rowCount () == 1 ? " task" : " tasks") - << std::endl; - else - std::cout << "No matches." - << std::endl; -} - -//////////////////////////////////////////////////////////////////////////////// -// Year Month Added Completed Deleted -// 2006 November 87 63 14 -// 2006 December 21 6 1 -time_t monthlyEpoch (const std::string& date) -{ - // Convert any date in epoch form to m/d/y, then convert back - // to epoch form for the date m/1/y. - if (date.length ()) - { - Date d1 (::atoi (date.c_str ())); - int m, d, y; - d1.toMDY (m, d, y); - Date d2 (m, 1, y); - time_t epoch; - d2.toEpoch (epoch); - return epoch; - } - - return 0; -} - -void handleReportHistory (const TDB& tdb, T& task, Config& conf) -{ - std::map groups; - std::map addedGroup; - std::map completedGroup; - std::map deletedGroup; - - // Scan the pending tasks. - tdb.gc (); - std::vector pending; - tdb.allPendingT (pending); - handleRecurrence (pending); - filter (pending, task); - for (unsigned int i = 0; i < pending.size (); ++i) - { - T task (pending[i]); - time_t epoch = monthlyEpoch (task.getAttribute ("entry")); - if (epoch) - { - groups[epoch] = 0; - - if (addedGroup.find (epoch) != addedGroup.end ()) - addedGroup[epoch] = addedGroup[epoch] + 1; - else - addedGroup[epoch] = 1; - - if (task.getStatus () == T::deleted) - { - epoch = monthlyEpoch (task.getAttribute ("end")); - - if (deletedGroup.find (epoch) != deletedGroup.end ()) - deletedGroup[epoch] = deletedGroup[epoch] + 1; - else - deletedGroup[epoch] = 1; - } - else if (task.getStatus () == T::completed) - { - epoch = monthlyEpoch (task.getAttribute ("end")); - - if (completedGroup.find (epoch) != completedGroup.end ()) - completedGroup[epoch] = completedGroup[epoch] + 1; - else - completedGroup[epoch] = 1; - } - } - } - - // Scan the completed tasks. - std::vector completed; - tdb.allCompletedT (completed); - filter (completed, task); - for (unsigned int i = 0; i < completed.size (); ++i) - { - T task (completed[i]); - time_t epoch = monthlyEpoch (task.getAttribute ("entry")); - if (epoch) - { - groups[epoch] = 0; - - if (addedGroup.find (epoch) != addedGroup.end ()) - addedGroup[epoch] = addedGroup[epoch] + 1; - else - addedGroup[epoch] = 1; - - epoch = monthlyEpoch (task.getAttribute ("end")); - if (task.getStatus () == T::deleted) - { - epoch = monthlyEpoch (task.getAttribute ("end")); - - if (deletedGroup.find (epoch) != deletedGroup.end ()) - deletedGroup[epoch] = deletedGroup[epoch] + 1; - else - deletedGroup[epoch] = 1; - } - else if (task.getStatus () == T::completed) - { - epoch = monthlyEpoch (task.getAttribute ("end")); - if (completedGroup.find (epoch) != completedGroup.end ()) - completedGroup[epoch] = completedGroup[epoch] + 1; - else - completedGroup[epoch] = 1; - } - } - } - - // Now build the table. - Table table; - table.setDateFormat (conf.get ("dateformat", "m/d/Y")); - table.addColumn ("Year"); - table.addColumn ("Month"); - table.addColumn ("Added"); - table.addColumn ("Completed"); - table.addColumn ("Deleted"); - table.addColumn ("Net"); - - if (conf.get ("color", true)) - { - table.setColumnUnderline (0); - table.setColumnUnderline (1); - table.setColumnUnderline (2); - table.setColumnUnderline (3); - table.setColumnUnderline (4); - table.setColumnUnderline (5); - } - - 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 (conf.get ("color", true) && net) - table.setCellFg (row, 5, net > 0 ? Text::red: Text::green); - } - - if (table.rowCount ()) - { - table.addRow (); - row = table.addRow (); - - table.addCell (row, 1, "Average"); - if (conf.get ("color", true)) table.setRowFg (row, Text::bold); - 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)); - } - - if (table.rowCount ()) - std::cout << optionalBlankLine (conf) - << table.render () - << std::endl; - else - std::cout << "No tasks." << std::endl; -} - -//////////////////////////////////////////////////////////////////////////////// -void handleReportGHistory (const TDB& tdb, T& task, Config& conf) -{ - // Determine window size, and set table accordingly. - int width = conf.get ("defaultwidth", 80); -#ifdef HAVE_LIBNCURSES - if (conf.get ("curses", true)) - { - WINDOW* w = initscr (); - width = w->_maxx + 1; - endwin (); - } -#endif - int widthOfBar = width - 15; // strlen ("2008 September ") - - std::map groups; - std::map addedGroup; - std::map completedGroup; - std::map deletedGroup; - - // Scan the pending tasks. - tdb.gc (); - std::vector pending; - tdb.allPendingT (pending); - handleRecurrence (pending); - filter (pending, task); - for (unsigned int i = 0; i < pending.size (); ++i) - { - T task (pending[i]); - time_t epoch = monthlyEpoch (task.getAttribute ("entry")); - if (epoch) - { - groups[epoch] = 0; - - if (addedGroup.find (epoch) != addedGroup.end ()) - addedGroup[epoch] = addedGroup[epoch] + 1; - else - addedGroup[epoch] = 1; - - if (task.getStatus () == T::deleted) - { - epoch = monthlyEpoch (task.getAttribute ("end")); - - if (deletedGroup.find (epoch) != deletedGroup.end ()) - deletedGroup[epoch] = deletedGroup[epoch] + 1; - else - deletedGroup[epoch] = 1; - } - else if (task.getStatus () == T::completed) - { - epoch = monthlyEpoch (task.getAttribute ("end")); - - if (completedGroup.find (epoch) != completedGroup.end ()) - completedGroup[epoch] = completedGroup[epoch] + 1; - else - completedGroup[epoch] = 1; - } - } - } - - // Scan the completed tasks. - std::vector completed; - tdb.allCompletedT (completed); - filter (completed, task); - for (unsigned int i = 0; i < completed.size (); ++i) - { - T task (completed[i]); - time_t epoch = monthlyEpoch (task.getAttribute ("entry")); - if (epoch) - { - groups[epoch] = 0; - - if (addedGroup.find (epoch) != addedGroup.end ()) - addedGroup[epoch] = addedGroup[epoch] + 1; - else - addedGroup[epoch] = 1; - - epoch = monthlyEpoch (task.getAttribute ("end")); - if (task.getStatus () == T::deleted) - { - epoch = monthlyEpoch (task.getAttribute ("end")); - - if (deletedGroup.find (epoch) != deletedGroup.end ()) - deletedGroup[epoch] = deletedGroup[epoch] + 1; - else - deletedGroup[epoch] = 1; - } - else if (task.getStatus () == T::completed) - { - epoch = monthlyEpoch (task.getAttribute ("end")); - if (completedGroup.find (epoch) != completedGroup.end ()) - completedGroup[epoch] = completedGroup[epoch] + 1; - else - completedGroup[epoch] = 1; - } - } - } - - // Now build the table. - Table table; - table.setDateFormat (conf.get ("dateformat", "m/d/Y")); - table.addColumn ("Year"); - table.addColumn ("Month"); - table.addColumn ("Added/Completed/Deleted"); - - if (conf.get ("color", true)) - { - table.setColumnUnderline (0); - table.setColumnUnderline (1); - } - - // Determine the longest line. - int maxLine = 0; - foreach (i, groups) - { - int line = addedGroup[i->first] + completedGroup[i->first] + deletedGroup[i->first]; - - if (line > maxLine) - maxLine = line; - } - - if (maxLine > 0) - { - 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 (conf.get ("color", true)) - { - 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; - } - - bar = Text::colorize (Text::black, Text::on_green, aBar); - bar += Text::colorize (Text::black, Text::on_yellow, cBar); - bar += Text::colorize (Text::black, Text::on_red, 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 += "-"; - - bar = aBar + cBar + dBar; - } - - table.addCell (row, 2, bar); - } - } - - if (table.rowCount ()) - { - std::cout << optionalBlankLine (conf) - << table.render () - << std::endl; - - if (conf.get ("color", true)) - std::cout << "Legend: " - << Text::colorize (Text::black, Text::on_green, "added") - << ", " - << Text::colorize (Text::black, Text::on_yellow, "completed") - << ", " - << Text::colorize (Text::black, Text::on_red, "deleted") - << optionalBlankLine (conf) - << std::endl; - else - std::cout << "Legend: + added, X completed, - deleted" << std::endl; - } - else - std::cout << "No tasks." << std::endl; -} - -//////////////////////////////////////////////////////////////////////////////// -// A summary of the command usage. Not useful to users, but used to display -// usage statistics for feedback. -// -// 2006-12-04 19:59:43 "task list" -// -void handleReportUsage (const TDB& tdb, T& task, Config& conf) -{ - if (conf.get ("command.logging") == "on") - { - std::map usage; - std::vector all; - tdb.logRead (all); - for (unsigned int i = 0; i < all.size (); ++i) - { - // 0123456789012345678901 - // v 21 - // 2006-12-04 19:59:43 "task list" - std::string command = all[i].substr (21, all[i].length () - 22); - - // Parse as a command line. - std::vector args; - split (args, command, " "); - - try - { - T task; - std::string commandName; - parse (args, commandName, task, conf); - - usage[commandName]++; - } - - // Deliberately ignore errors from parsing the command log, as there may - // be commands from a prior version of task in there, which were - // abbreviated, and are now ambiguous. - catch (...) {} - } - - // Now render the table. - Table table; - table.addColumn ("Command"); - table.addColumn ("Frequency"); - - if (conf.get ("color", true)) - { - table.setColumnUnderline (0); - table.setColumnUnderline (1); - } - - table.setColumnJustification (1, Table::right); - table.sortOn (1, Table::descendingNumeric); - table.setDateFormat (conf.get ("dateformat", "m/d/Y")); - - foreach (i, usage) - { - int row = table.addRow (); - table.addCell (row, 0, (i->first == "" ? "(modify)" : i->first)); - table.addCell (row, 1, i->second); - } - - if (table.rowCount ()) - std::cout << optionalBlankLine (conf) - << table.render () - << std::endl; - else - std::cout << "No usage." << std::endl; - } - else - std::cout << "Command logging is not enabled, so no history has been kept." - << std::endl; -} - -//////////////////////////////////////////////////////////////////////////////// -std::string renderMonths ( - int firstMonth, - int firstYear, - const Date& today, - std::vector & all, - Config& conf) -{ - Table table; - table.setDateFormat (conf.get ("dateformat", "m/d/Y")); - int monthsPerLine = (conf.get ("monthsperline", 1)); - - // Build table for the number of months to be displayed. - for (int i = 0 ; i < (monthsPerLine * 8); i += 8) - { - table.addColumn (" "); - table.addColumn ("Su"); - table.addColumn ("Mo"); - table.addColumn ("Tu"); - table.addColumn ("We"); - table.addColumn ("Th"); - table.addColumn ("Fr"); - table.addColumn ("Sa"); - - if (conf.get ("color", true)) - { - table.setColumnUnderline (i + 1); - table.setColumnUnderline (i + 2); - table.setColumnUnderline (i + 3); - table.setColumnUnderline (i + 4); - table.setColumnUnderline (i + 5); - table.setColumnUnderline (i + 6); - table.setColumnUnderline (i + 7); - } - - table.setColumnJustification (i + 0, Table::right); - table.setColumnJustification (i + 1, Table::right); - table.setColumnJustification (i + 2, Table::right); - table.setColumnJustification (i + 3, Table::right); - table.setColumnJustification (i + 4, Table::right); - table.setColumnJustification (i + 5, Table::right); - table.setColumnJustification (i + 6, Table::right); - table.setColumnJustification (i + 7, Table::right); - } - - // At most, we need 6 rows. - table.addRow (); - table.addRow (); - table.addRow (); - table.addRow (); - table.addRow (); - table.addRow (); - - // Set number of days per month, months to render, and years to render. - std::vector years; - std::vector months; - std::vector daysInMonth; - int thisYear = firstYear; - int thisMonth = firstMonth; - for (int i = 0 ; i < monthsPerLine ; i++) - { - if (thisMonth < 13) - { - years.push_back (thisYear); - } - else - { - thisMonth -= 12; - years.push_back (++thisYear); - } - months.push_back (thisMonth); - daysInMonth.push_back (Date::daysInMonth (thisMonth++, thisYear)); - } - - int row = 0; - - // Loop through months to be added on this line. - for (int c = 0; c < monthsPerLine ; c++) - { - // Reset row counter for subsequent months - if (c != 0) - row = 0; - - // Loop through days in month and add to table. - for (int d = 1; d <= daysInMonth.at (c); ++d) - { - Date temp (months.at (c), d, years.at (c)); - int dow = temp.dayOfWeek (); - int thisCol = dow + 1 + (8 * c); - - table.addCell (row, thisCol, d); - - if (conf.get ("color", true) && - today.day () == d && - today.month () == months.at (c) && - today.year () == years.at (c)) - table.setCellFg (row, thisCol, Text::cyan); - - std::vector ::iterator it; - for (it = all.begin (); it != all.end (); ++it) - { - Date due (::atoi (it->getAttribute ("due").c_str ())); - - if (conf.get ("color", true) && - due.day () == d && - due.month () == months.at (c) && - due.year () == years.at (c)) - { - table.setCellFg (row, thisCol, Text::black); - table.setCellBg (row, thisCol, due < today ? Text::on_red : Text::on_yellow); - } - } - - // Check for end of week, and... - if (dow == 6 && d < daysInMonth.at (c)) - row++; - } - } - - return table.render (); -} - -//////////////////////////////////////////////////////////////////////////////// -void handleReportCalendar (const TDB& tdb, T& task, Config& conf) -{ - // Load all the pending tasks. - tdb.gc (); - std::vector pending; - tdb.allPendingT (pending); - handleRecurrence (pending); - filter (pending, task); - - // Find the oldest pending due date. - Date oldest; - Date newest; - std::vector ::iterator it; - for (it = pending.begin (); it != pending.end (); ++it) - { - if (it->getAttribute ("due") != "") - { - Date d (::atoi (it->getAttribute ("due").c_str ())); - - if (d < oldest) oldest = d; - if (d > newest) newest = d; - } - } - - // Iterate from oldest due month, year to newest month, year. - Date today; - int mFrom = oldest.month (); - int yFrom = oldest.year (); - - int mTo = newest.month (); - int yTo = newest.year (); - - std::cout << std::endl; - std::string output; - - int monthsPerLine = (conf.get ("monthsperline", 1)); - - 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++) - { - std::string month = Date::monthName (nextM); - std::cout << month - << " " - << std::setw(23 // one month's output width - - month.length ()// month name length - - 1)// spacer character - << std::left - << nextY; - - if (++nextM > 12) - { - nextM = 1; - nextY++; - } - } - - std::cout << std::endl - << optionalBlankLine (conf) - << renderMonths (mFrom, yFrom, today, pending, conf) - << std::endl; - - mFrom += monthsPerLine; - if (mFrom > 12) - { - mFrom -= 12; - ++yFrom; - } - } - - std::cout << "Legend: " - << Text::colorize (Text::cyan, Text::nocolor, "today") - << ", " - << Text::colorize (Text::black, Text::on_yellow, "due") - << ", " - << Text::colorize (Text::black, Text::on_red, "overdue") - << "." - << optionalBlankLine (conf) - << std::endl; -} - -//////////////////////////////////////////////////////////////////////////////// -void handleReportActive (const TDB& tdb, T& task, Config& conf) -{ - // Determine window size, and set table accordingly. - int width = conf.get ("defaultwidth", 80); -#ifdef HAVE_LIBNCURSES - if (conf.get ("curses", true)) - { - WINDOW* w = initscr (); - width = w->_maxx + 1; - endwin (); - } -#endif - - // Get all the tasks. - tdb.gc (); - std::vector tasks; - tdb.pendingT (tasks); - filter (tasks, task); - - initializeColorRules (conf); - - // Create a table for output. - Table table; - table.setTableWidth (width); - table.setDateFormat (conf.get ("dateformat", "m/d/Y")); - table.addColumn ("ID"); - table.addColumn ("Project"); - table.addColumn ("Pri"); - table.addColumn ("Due"); - table.addColumn ("Description"); - - if (conf.get ("color", true)) - { - table.setColumnUnderline (0); - table.setColumnUnderline (1); - table.setColumnUnderline (2); - table.setColumnUnderline (3); - table.setColumnUnderline (4); - } - - table.setColumnWidth (0, Table::minimum); - table.setColumnWidth (1, Table::minimum); - table.setColumnWidth (2, Table::minimum); - table.setColumnWidth (3, Table::minimum); - table.setColumnWidth (4, Table::flexible); - - table.setColumnJustification (0, Table::right); - table.setColumnJustification (3, Table::right); - - table.sortOn (3, Table::ascendingDate); - table.sortOn (2, Table::descendingPriority); - table.sortOn (1, Table::ascendingCharacter); - - // Iterate over each task, and apply selection criteria. - for (unsigned int i = 0; i < tasks.size (); ++i) - { - T refTask (tasks[i]); - if (refTask.getAttribute ("start") != "") - { - Date now; - bool imminent = false; - bool overdue = false; - std::string due = refTask.getAttribute ("due"); - if (due.length ()) - { - switch (getDueState (due)) - { - case 2: overdue = true; break; - case 1: imminent = true; break; - case 0: - default: break; - } - - Date dt (::atoi (due.c_str ())); - due = dt.toString (conf.get ("dateformat", "m/d/Y")); - } - - // All criteria match, so add refTask to the output table. - int row = table.addRow (); - table.addCell (row, 0, refTask.getId ()); - table.addCell (row, 1, refTask.getAttribute ("project")); - table.addCell (row, 2, refTask.getAttribute ("priority")); - table.addCell (row, 3, due); - table.addCell (row, 4, refTask.getDescription ()); - - if (conf.get ("color", true)) - { - Text::color fg = Text::colorCode (refTask.getAttribute ("fg")); - Text::color bg = Text::colorCode (refTask.getAttribute ("bg")); - autoColorize (refTask, fg, bg); - table.setRowFg (row, fg); - table.setRowBg (row, bg); - - if (fg == Text::nocolor) - { - if (overdue) - table.setCellFg (row, 3, Text::red); - else if (imminent) - table.setCellFg (row, 3, Text::yellow); - } - } - } - } - - if (table.rowCount ()) - std::cout << optionalBlankLine (conf) - << table.render () - << optionalBlankLine (conf) - << table.rowCount () - << (table.rowCount () == 1 ? " task" : " tasks") - << std::endl; - else - std::cout << "No active tasks." << std::endl; -} - -//////////////////////////////////////////////////////////////////////////////// -void handleReportOverdue (const TDB& tdb, T& task, Config& conf) -{ - // Determine window size, and set table accordingly. - int width = conf.get ("defaultwidth", 80); -#ifdef HAVE_LIBNCURSES - if (conf.get ("curses", true)) - { - WINDOW* w = initscr (); - width = w->_maxx + 1; - endwin (); - } -#endif - - // Get all the tasks. - std::vector tasks; - tdb.pendingT (tasks); - filter (tasks, task); - - initializeColorRules (conf); - - // Create a table for output. - Table table; - table.setTableWidth (width); - table.setDateFormat (conf.get ("dateformat", "m/d/Y")); - table.addColumn ("ID"); - table.addColumn ("Project"); - table.addColumn ("Pri"); - table.addColumn ("Due"); - table.addColumn ("Description"); - - if (conf.get ("color", true)) - { - table.setColumnUnderline (0); - table.setColumnUnderline (1); - table.setColumnUnderline (2); - table.setColumnUnderline (3); - table.setColumnUnderline (4); - } - - table.setColumnWidth (0, Table::minimum); - table.setColumnWidth (1, Table::minimum); - table.setColumnWidth (2, Table::minimum); - table.setColumnWidth (3, Table::minimum); - table.setColumnWidth (4, Table::flexible); - - table.setColumnJustification (0, Table::right); - table.setColumnJustification (3, Table::right); - - table.sortOn (3, Table::ascendingDate); - table.sortOn (2, Table::descendingPriority); - table.sortOn (1, Table::ascendingCharacter); - - Date now; - - // Iterate over each task, and apply selection criteria. - for (unsigned int i = 0; i < tasks.size (); ++i) - { - T refTask (tasks[i]); - std::string due; - if ((due = refTask.getAttribute ("due")) != "") - { - if (due.length ()) - { - Date dt (::atoi (due.c_str ())); - due = dt.toString (conf.get ("dateformat", "m/d/Y")); - - // If overdue. - if (dt < now) - { - // All criteria match, so add refTask to the output table. - int row = table.addRow (); - table.addCell (row, 0, refTask.getId ()); - table.addCell (row, 1, refTask.getAttribute ("project")); - table.addCell (row, 2, refTask.getAttribute ("priority")); - table.addCell (row, 3, due); - table.addCell (row, 4, refTask.getDescription ()); - - if (conf.get ("color", true)) - { - Text::color fg = Text::colorCode (refTask.getAttribute ("fg")); - Text::color bg = Text::colorCode (refTask.getAttribute ("bg")); - autoColorize (refTask, fg, bg); - table.setRowFg (row, fg); - table.setRowBg (row, bg); - - if (fg == Text::nocolor) - table.setCellFg (row, 3, Text::red); - } - } - } - } - } - - if (table.rowCount ()) - std::cout << optionalBlankLine (conf) - << table.render () - << optionalBlankLine (conf) - << table.rowCount () - << (table.rowCount () == 1 ? " task" : " tasks") - << std::endl; - else - std::cout << "No overdue tasks." << std::endl; -} - -//////////////////////////////////////////////////////////////////////////////// -// Successively apply filters based on the task object built from the command -// line. Tasks that match all the specified criteria are listed. -void handleReportOldest (const TDB& tdb, T& task, Config& conf) -{ - // Determine window size, and set table accordingly. - int width = conf.get ("defaultwidth", 80); -#ifdef HAVE_LIBNCURSES - if (conf.get ("curses", true)) - { - WINDOW* w = initscr (); - width = w->_maxx + 1; - endwin (); - } -#endif - - // Get the pending tasks. - tdb.gc (); - std::vector tasks; - tdb.allPendingT (tasks); - handleRecurrence (tasks); - filter (tasks, task); - - initializeColorRules (conf); - - bool showAge = conf.get ("showage", true); - unsigned int quantity = conf.get ("oldest", 10); - - // Create a table for output. - Table table; - table.setTableWidth (width); - table.addColumn ("ID"); - table.addColumn ("Project"); - table.addColumn ("Pri"); - table.addColumn ("Due"); - table.addColumn ("Active"); - if (showAge) table.addColumn ("Age"); - table.addColumn ("Description"); - - if (conf.get ("color", true)) - { - table.setColumnUnderline (0); - table.setColumnUnderline (1); - table.setColumnUnderline (2); - table.setColumnUnderline (3); - table.setColumnUnderline (4); - table.setColumnUnderline (5); - if (showAge) table.setColumnUnderline (6); - } - - table.setColumnWidth (0, Table::minimum); - table.setColumnWidth (1, Table::minimum); - table.setColumnWidth (2, Table::minimum); - table.setColumnWidth (3, Table::minimum); - table.setColumnWidth (4, Table::minimum); - if (showAge) table.setColumnWidth (5, Table::minimum); - table.setColumnWidth ((showAge ? 6 : 5), Table::flexible); - - table.setColumnJustification (0, Table::right); - table.setColumnJustification (3, Table::right); - if (showAge) table.setColumnJustification (5, Table::right); - - table.sortOn (3, Table::ascendingDate); - table.sortOn (2, Table::descendingPriority); - table.sortOn (1, Table::ascendingCharacter); - - table.setDateFormat (conf.get ("dateformat", "m/d/Y")); - - for (unsigned int i = 0; i < min (quantity, tasks.size ()); ++i) - { - T refTask (tasks[i]); - Date now; - - // Now format the matching task. - bool imminent = false; - bool overdue = false; - std::string due = refTask.getAttribute ("due"); - if (due.length ()) - { - switch (getDueState (due)) - { - case 2: overdue = true; break; - case 1: imminent = true; break; - case 0: - default: break; - } - - Date dt (::atoi (due.c_str ())); - due = dt.toString (conf.get ("dateformat", "m/d/Y")); - } - - std::string active; - if (refTask.getAttribute ("start") != "") - active = "*"; - - std::string age; - std::string created = refTask.getAttribute ("entry"); - if (created.length ()) - { - Date dt (::atoi (created.c_str ())); - formatTimeDeltaDays (age, (time_t) (now - dt)); - } - - // All criteria match, so add refTask to the output table. - int row = table.addRow (); - table.addCell (row, 0, refTask.getId ()); - table.addCell (row, 1, refTask.getAttribute ("project")); - table.addCell (row, 2, refTask.getAttribute ("priority")); - table.addCell (row, 3, due); - table.addCell (row, 4, active); - if (showAge) table.addCell (row, 5, age); - table.addCell (row, (showAge ? 6 : 5), refTask.getDescription ()); - - if (conf.get ("color", true)) - { - Text::color fg = Text::colorCode (refTask.getAttribute ("fg")); - Text::color bg = Text::colorCode (refTask.getAttribute ("bg")); - autoColorize (refTask, fg, bg); - table.setRowFg (row, fg); - table.setRowBg (row, bg); - - if (fg == Text::nocolor) - { - if (overdue) - table.setCellFg (row, 3, Text::red); - else if (imminent) - table.setCellFg (row, 3, Text::yellow); - } - } - } - - if (table.rowCount ()) - std::cout << optionalBlankLine (conf) - << table.render () - << optionalBlankLine (conf) - << table.rowCount () - << (table.rowCount () == 1 ? " task" : " tasks") - << std::endl; - else - std::cout << "No matches." - << std::endl; -} - -//////////////////////////////////////////////////////////////////////////////// -// Successively apply filters based on the task object built from the command -// line. Tasks that match all the specified criteria are listed. -void handleReportNewest (const TDB& tdb, T& task, Config& conf) -{ - // Determine window size, and set table accordingly. - int width = conf.get ("defaultwidth", 80); -#ifdef HAVE_LIBNCURSES - if (conf.get ("curses", true)) - { - WINDOW* w = initscr (); - width = w->_maxx + 1; - endwin (); - } -#endif - - // Get the pending tasks. - tdb.gc (); - std::vector tasks; - tdb.allPendingT (tasks); - handleRecurrence (tasks); - filter (tasks, task); - - initializeColorRules (conf); - - bool showAge = conf.get ("showage", true); - int quantity = conf.get ("newest", 10); - - // Create a table for output. - Table table; - table.setTableWidth (width); - table.addColumn ("ID"); - table.addColumn ("Project"); - table.addColumn ("Pri"); - table.addColumn ("Due"); - table.addColumn ("Active"); - if (showAge) table.addColumn ("Age"); - table.addColumn ("Description"); - - if (conf.get ("color", true)) - { - table.setColumnUnderline (0); - table.setColumnUnderline (1); - table.setColumnUnderline (2); - table.setColumnUnderline (3); - table.setColumnUnderline (4); - table.setColumnUnderline (5); - if (showAge) table.setColumnUnderline (6); - } - - table.setColumnWidth (0, Table::minimum); - table.setColumnWidth (1, Table::minimum); - table.setColumnWidth (2, Table::minimum); - table.setColumnWidth (3, Table::minimum); - table.setColumnWidth (4, Table::minimum); - if (showAge) table.setColumnWidth (5, Table::minimum); - table.setColumnWidth ((showAge ? 6 : 5), Table::flexible); - - table.setColumnJustification (0, Table::right); - table.setColumnJustification (3, Table::right); - if (showAge) table.setColumnJustification (5, Table::right); - - table.sortOn (3, Table::ascendingDate); - table.sortOn (2, Table::descendingPriority); - table.sortOn (1, Table::ascendingCharacter); - - table.setDateFormat (conf.get ("dateformat", "m/d/Y")); - - int total = tasks.size (); - for (int i = total - 1; i >= max (0, total - quantity); --i) - { - T refTask (tasks[i]); - Date now; - - // Now format the matching task. - bool imminent = false; - bool overdue = false; - std::string due = refTask.getAttribute ("due"); - if (due.length ()) - { - switch (getDueState (due)) - { - case 2: overdue = true; break; - case 1: imminent = true; break; - case 0: - default: break; - } - - Date dt (::atoi (due.c_str ())); - due = dt.toString (conf.get ("dateformat", "m/d/Y")); - } - - std::string active; - if (refTask.getAttribute ("start") != "") - active = "*"; - - std::string age; - std::string created = refTask.getAttribute ("entry"); - if (created.length ()) - { - Date dt (::atoi (created.c_str ())); - formatTimeDeltaDays (age, (time_t) (now - dt)); - } - - // All criteria match, so add refTask to the output table. - int row = table.addRow (); - table.addCell (row, 0, refTask.getId ()); - table.addCell (row, 1, refTask.getAttribute ("project")); - table.addCell (row, 2, refTask.getAttribute ("priority")); - table.addCell (row, 3, due); - table.addCell (row, 4, active); - if (showAge) table.addCell (row, 5, age); - table.addCell (row, (showAge ? 6 : 5), refTask.getDescription ()); - - if (conf.get ("color", true)) - { - Text::color fg = Text::colorCode (refTask.getAttribute ("fg")); - Text::color bg = Text::colorCode (refTask.getAttribute ("bg")); - autoColorize (refTask, fg, bg); - table.setRowFg (row, fg); - table.setRowBg (row, bg); - - if (fg == Text::nocolor) - { - if (overdue) - table.setCellFg (row, 3, Text::red); - else if (imminent) - table.setCellFg (row, 3, Text::yellow); - } - } - } - - if (table.rowCount ()) - std::cout << optionalBlankLine (conf) - << table.render () - << optionalBlankLine (conf) - << table.rowCount () - << (table.rowCount () == 1 ? " task" : " tasks") - << std::endl; - else - std::cout << "No matches." - << std::endl; -} - - -//////////////////////////////////////////////////////////////////////////////// -void handleReportStats (const TDB& tdb, T& task, Config& conf) -{ - // Get all the tasks. - std::vector tasks; - tdb.allT (tasks); - filter (tasks, task); - - Date now; - time_t earliest = time (NULL); - time_t latest = 1; - int totalT = 0; - int deletedT = 0; - int pendingT = 0; - int completedT = 0; - int taggedT = 0; - int recurringT = 0; - float daysPending = 0.0; - int descLength = 0; - - std::vector ::iterator it; - for (it = tasks.begin (); it != tasks.end (); ++it) - { - ++totalT; - if (it->getStatus () == T::deleted) ++deletedT; - if (it->getStatus () == T::pending) ++pendingT; - if (it->getStatus () == T::completed) ++completedT; - if (it->getStatus () == T::recurring) ++recurringT; - - time_t entry = ::atoi (it->getAttribute ("entry").c_str ()); - if (entry < earliest) earliest = entry; - if (entry > latest) latest = entry; - - if (it->getStatus () == T::completed) - { - time_t end = ::atoi (it->getAttribute ("end").c_str ()); - daysPending += (end - entry) / 86400.0; - } - - if (it->getStatus () == T::pending) - daysPending += (now - entry) / 86400.0; - - descLength += it->getDescription ().length (); - - std::vector tags; - it->getTags (tags); - if (tags.size ()) ++taggedT; - } - - std::cout << "Pending " << pendingT << std::endl - << "Recurring " << recurringT << std::endl - << "Completed " << completedT << std::endl - << "Deleted " << deletedT << std::endl - << "Total " << totalT << std::endl; - - if (tasks.size ()) - { - Date e (earliest); - std::cout << "Oldest task " << e.toString (conf.get ("dateformat", "m/d/Y")) << std::endl; - Date l (latest); - std::cout << "Newest task " << l.toString (conf.get ("dateformat", "m/d/Y")) << std::endl; - std::cout << "Task used for " << formatSeconds (latest - earliest) << std::endl; - } - - if (totalT) - std::cout << "Task added every " << formatSeconds ((latest - earliest) / totalT) << std::endl; - - if (completedT) - std::cout << "Task completed every " << formatSeconds ((latest - earliest) / completedT) << std::endl; - - if (deletedT) - std::cout << "Task deleted every " << formatSeconds ((latest - earliest) / deletedT) << std::endl; - - if (pendingT || completedT) - std::cout << "Average time pending " - << formatSeconds ((int) ((daysPending / (pendingT + completedT)) * 86400)) - << std::endl; - - if (totalT) - { - std::cout << "Average desc length " << (int) (descLength / totalT) << " characters" << std::endl; - std::cout << "Tasks tagged " << std::setprecision (3) << (100.0 * taggedT / totalT) << "%" << std::endl; - } -} - -//////////////////////////////////////////////////////////////////////////////// -void handleVersion (Config& conf) -{ - // Determine window size, and set table accordingly. - int width = conf.get ("defaultwidth", 80); -#ifdef HAVE_LIBNCURSES - if (conf.get ("curses", true)) - { - WINDOW* w = initscr (); - width = w->_maxx + 1; - endwin (); - } -#endif - - // Create a table for output. - Table table; - table.setTableWidth (width); - table.setDateFormat (conf.get ("dateformat", "m/d/Y")); - table.addColumn ("Config variable"); - table.addColumn ("Value"); - - if (conf.get ("color", true)) - { - table.setColumnUnderline (0); - table.setColumnUnderline (1); - } - - 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); - - std::vector all; - conf.all (all); - foreach (i, all) - { - std::string value = conf.get (*i); - if (value != "") - { - int row = table.addRow (); - table.addCell (row, 0, *i); - table.addCell (row, 1, value); - } - } - - std::cout << "Copyright (C) 2006 - 2008, P. Beckingham." - << std::endl - << PACKAGE - << " " - << VERSION - << std::endl - << std::endl - << "Task comes with ABSOLUTELY NO WARRANTY; for details read the COPYING file" - << std::endl - << "included. This is free software, and you are welcome to redistribute it" - << std::endl - << "under certain conditions; again, see the COPYING file for details." - << std::endl - << std::endl - << table.render () - << std::endl; - - // Verify installation. This is mentioned in the documentation as the way to - // ensure everything is properly installed. - - if (all.size () == 0) - std::cout << "Configuration error: .taskrc contains no entries" - << std::endl; - else - { - if (conf.get ("data.location") == "") - std::cout << "Configuration error: data.location not specified in .taskrc " - "file." - << std::endl; - - if (access (conf.get ("data.location").c_str (), X_OK)) - std::cout << "Configuration error: data.location contains a directory name" - " that doesn't exist, or is unreadable." - << std::endl; - } -} - -//////////////////////////////////////////////////////////////////////////////// -void handleDelete (const TDB& tdb, T& task, Config& conf) -{ - if (conf.get ("confirmation") != "yes" || confirm ("Permanently delete task?")) - { - // Check for the more complex case of a recurring task. - std::string parent = task.getAttribute ("parent"); - - // If this is a recurring task, get confirmation to delete them all. - if (parent != "" && - confirm ("This is a recurring task. Do you want to delete all pending recurrences of this same task?")) - { - // Scan all pending tasks for siblings of this task, and the parent - // itself, and delete them. - std::vector all; - tdb.allPendingT (all); - std::vector ::iterator it; - for (it = all.begin (); it != all.end (); ++it) - if (it->getAttribute ("parent") == parent || - it->getUUID () == parent) - tdb.deleteT (*it); - } - - // No confirmation, just delete the one. - else - tdb.deleteT (task); - } - else - std::cout << "Task not deleted." << std::endl; -} - -//////////////////////////////////////////////////////////////////////////////// -void handleStart (const TDB& tdb, T& task, Config& conf) -{ - std::vector all; - tdb.pendingT (all); - - std::vector ::iterator it; - for (it = all.begin (); it != all.end (); ++it) - { - if (it->getId () == task.getId ()) - { - T original (*it); - - if (original.getAttribute ("start") == "") - { - char startTime[16]; - sprintf (startTime, "%u", (unsigned int) time (NULL)); - original.setAttribute ("start", startTime); - - original.setId (task.getId ()); - tdb.modifyT (original); - - nag (tdb, task, conf); - return; - } - else - std::cout << "Task " << task.getId () << " already started." << std::endl; - } - } - - throw std::string ("Task not found."); -} - -//////////////////////////////////////////////////////////////////////////////// -void handleDone (const TDB& tdb, T& task, Config& conf) -{ - if (!tdb.completeT (task)) - throw std::string ("Could not mark task as completed."); - - nag (tdb, task, conf); -} - -//////////////////////////////////////////////////////////////////////////////// -void handleExport (const TDB& tdb, T& task, Config& conf) -{ - // Use the description as a file name, then clobber the description so the - // file name isn't used for filtering. - std::string file = trim (task.getDescription ()); - task.setDescription (""); - - if (file.length () > 0) - { - std::ofstream out (file.c_str ()); - if (out.good ()) - { - out << "'id'," - << "'status'," - << "'tags'," - << "'entry'," - << "'start'," - << "'due'," - << "'end'," - << "'project'," - << "'priority'," - << "'fg'," - << "'bg'," - << "'description'" - << "\n"; - - std::vector all; - tdb.allT (all); - filter (all, task); - foreach (t, all) - { - out << t->composeCSV ().c_str (); - } - out.close (); - } - else - throw std::string ("Could not write to export file."); - } - else - throw std::string ("You must specify a file to write to."); -} - -//////////////////////////////////////////////////////////////////////////////// -void handleModify (const TDB& tdb, T& task, Config& conf) -{ - std::vector all; - tdb.pendingT (all); - - std::vector ::iterator it; - for (it = all.begin (); it != all.end (); ++it) - { - if (it->getId () == task.getId ()) - { - T original (*it); - - // A non-zero value forces a file write. - int changes = 0; - - // Apply a new description, if any. - if (task.getDescription () != "") - { - original.setDescription (task.getDescription ()); - ++changes; - } - - // Apply or remove tags, if any. - std::vector tags; - task.getTags (tags); - for (unsigned int i = 0; i < tags.size (); ++i) - { - if (tags[i][0] == '+') - original.addTag (tags[i].substr (1, std::string::npos)); - else - original.addTag (tags[i]); - - ++changes; - } - - task.getRemoveTags (tags); - for (unsigned int i = 0; i < tags.size (); ++i) - { - if (tags[i][0] == '-') - original.removeTag (tags[i].substr (1, std::string::npos)); - else - original.removeTag (tags[i]); - - ++changes; - } - - // Apply or remove attributes, if any. - std::map attributes; - task.getAttributes (attributes); - foreach (i, attributes) - { - if (i->second == "") - original.removeAttribute (i->first); - else - original.setAttribute (i->first, i->second); - - ++changes; - } - - std::string from; - std::string to; - task.getSubstitution (from, to); - if (from != "") - { - std::string description = original.getDescription (); - size_t pattern = description.find (from); - if (pattern != std::string::npos) - { - description = description.substr (0, pattern) + - to + - description.substr (pattern + from.length (), std::string::npos); - original.setDescription (description); - ++changes; - } - } - - if (changes) - { - original.setId (task.getId ()); - tdb.modifyT (original); - } - - return; - } - } - - throw std::string ("Task not found."); -} - -//////////////////////////////////////////////////////////////////////////////// -void handleColor (Config& conf) -{ - if (conf.get ("color", true)) - { - std::cout << optionalBlankLine (conf) << "Foreground" << std::endl - << " " - << Text::colorize (Text::bold, Text::nocolor, "bold") << " " - << Text::colorize (Text::underline, Text::nocolor, "underline") << " " - << Text::colorize (Text::bold_underline, Text::nocolor, "bold_underline") << std::endl - - << " " << Text::colorize (Text::black, Text::nocolor, "black") << " " - << Text::colorize (Text::bold_black, Text::nocolor, "bold_black") << " " - << Text::colorize (Text::underline_black, Text::nocolor, "underline_black") << " " - << Text::colorize (Text::bold_underline_black, Text::nocolor, "bold_underline_black") << std::endl - - << " " << Text::colorize (Text::red, Text::nocolor, "red") << " " - << Text::colorize (Text::bold_red, Text::nocolor, "bold_red") << " " - << Text::colorize (Text::underline_red, Text::nocolor, "underline_red") << " " - << Text::colorize (Text::bold_underline_red, Text::nocolor, "bold_underline_red") << std::endl - - << " " << Text::colorize (Text::green, Text::nocolor, "green") << " " - << Text::colorize (Text::bold_green, Text::nocolor, "bold_green") << " " - << Text::colorize (Text::underline_green, Text::nocolor, "underline_green") << " " - << Text::colorize (Text::bold_underline_green, Text::nocolor, "bold_underline_green") << std::endl - - << " " << Text::colorize (Text::yellow, Text::nocolor, "yellow") << " " - << Text::colorize (Text::bold_yellow, Text::nocolor, "bold_yellow") << " " - << Text::colorize (Text::underline_yellow, Text::nocolor, "underline_yellow") << " " - << Text::colorize (Text::bold_underline_yellow, Text::nocolor, "bold_underline_yellow") << std::endl - - << " " << Text::colorize (Text::blue, Text::nocolor, "blue") << " " - << Text::colorize (Text::bold_blue, Text::nocolor, "bold_blue") << " " - << Text::colorize (Text::underline_blue, Text::nocolor, "underline_blue") << " " - << Text::colorize (Text::bold_underline_blue, Text::nocolor, "bold_underline_blue") << std::endl - - << " " << Text::colorize (Text::magenta, Text::nocolor, "magenta") << " " - << Text::colorize (Text::bold_magenta, Text::nocolor, "bold_magenta") << " " - << Text::colorize (Text::underline_magenta, Text::nocolor, "underline_magenta") << " " - << Text::colorize (Text::bold_underline_magenta, Text::nocolor, "bold_underline_magenta") << std::endl - - << " " << Text::colorize (Text::cyan, Text::nocolor, "cyan") << " " - << Text::colorize (Text::bold_cyan, Text::nocolor, "bold_cyan") << " " - << Text::colorize (Text::underline_cyan, Text::nocolor, "underline_cyan") << " " - << Text::colorize (Text::bold_underline_cyan, Text::nocolor, "bold_underline_cyan") << std::endl - - << " " << Text::colorize (Text::white, Text::nocolor, "white") << " " - << Text::colorize (Text::bold_white, Text::nocolor, "bold_white") << " " - << Text::colorize (Text::underline_white, Text::nocolor, "underline_white") << " " - << Text::colorize (Text::bold_underline_white, Text::nocolor, "bold_underline_white") << std::endl - - << std::endl << "Background" << std::endl - << " " << Text::colorize (Text::nocolor, Text::on_black, "on_black") << " " - << Text::colorize (Text::nocolor, Text::on_bright_black, "on_bright_black") << std::endl - - << " " << Text::colorize (Text::nocolor, Text::on_red, "on_red") << " " - << Text::colorize (Text::nocolor, Text::on_bright_red, "on_bright_red") << std::endl - - << " " << Text::colorize (Text::nocolor, Text::on_green, "on_green") << " " - << Text::colorize (Text::nocolor, Text::on_bright_green, "on_bright_green") << std::endl - - << " " << Text::colorize (Text::nocolor, Text::on_yellow, "on_yellow") << " " - << Text::colorize (Text::nocolor, Text::on_bright_yellow, "on_bright_yellow") << std::endl - - << " " << Text::colorize (Text::nocolor, Text::on_blue, "on_blue") << " " - << Text::colorize (Text::nocolor, Text::on_bright_blue, "on_bright_blue") << std::endl - - << " " << Text::colorize (Text::nocolor, Text::on_magenta, "on_magenta") << " " - << Text::colorize (Text::nocolor, Text::on_bright_magenta, "on_bright_magenta") << std::endl - - << " " << Text::colorize (Text::nocolor, Text::on_cyan, "on_cyan") << " " - << Text::colorize (Text::nocolor, Text::on_bright_cyan, "on_bright_cyan") << std::endl - - << " " << Text::colorize (Text::nocolor, Text::on_white, "on_white") << " " - << Text::colorize (Text::nocolor, Text::on_bright_white, "on_bright_white") << std::endl - - << optionalBlankLine (conf); - } - else - { - std::cout << "Color is currently turned off in your .taskrc file." << std::endl; - } -} - -//////////////////////////////////////////////////////////////////////////////// -void gatherNextTasks ( - const TDB& tdb, - T& task, - Config& conf, - std::vector & pending, - std::vector & all) -{ - // For counting tasks by project. - std::map countByProject; - std::map matching; - - Date now; - - // How many items per project? Default 3. - int limit = conf.get ("next", 3); - - // due:< 1wk, pri:* - for (unsigned int i = 0; i < pending.size (); ++i) - { - if (pending[i].getStatus () == T::pending) - { - std::string due = pending[i].getAttribute ("due"); - if (due != "") - { - Date d (::atoi (due.c_str ())); - if (d < now + (7 * 24 * 60 * 60)) // if due:< 1wk - { - std::string project = pending[i].getAttribute ("project"); - if (countByProject[project] < limit && matching.find (i) == matching.end ()) - { - ++countByProject[project]; - matching[i] = true; - } - } - } - } - } - - // due:*, pri:H - for (unsigned int i = 0; i < pending.size (); ++i) - { - if (pending[i].getStatus () == T::pending) - { - std::string due = pending[i].getAttribute ("due"); - if (due != "") - { - std::string priority = pending[i].getAttribute ("priority"); - if (priority == "H") - { - std::string project = pending[i].getAttribute ("project"); - if (countByProject[project] < limit && matching.find (i) == matching.end ()) - { - ++countByProject[project]; - matching[i] = true; - } - } - } - } - } - - // pri:H - for (unsigned int i = 0; i < pending.size (); ++i) - { - if (pending[i].getStatus () == T::pending) - { - std::string priority = pending[i].getAttribute ("priority"); - if (priority == "H") - { - std::string project = pending[i].getAttribute ("project"); - if (countByProject[project] < limit && matching.find (i) == matching.end ()) - { - ++countByProject[project]; - matching[i] = true; - } - } - } - } - - // due:*, pri:M - for (unsigned int i = 0; i < pending.size (); ++i) - { - if (pending[i].getStatus () == T::pending) - { - std::string due = pending[i].getAttribute ("due"); - if (due != "") - { - std::string priority = pending[i].getAttribute ("priority"); - if (priority == "M") - { - std::string project = pending[i].getAttribute ("project"); - if (countByProject[project] < limit && matching.find (i) == matching.end ()) - { - ++countByProject[project]; - matching[i] = true; - } - } - } - } - } - - // pri:M - for (unsigned int i = 0; i < pending.size (); ++i) - { - if (pending[i].getStatus () == T::pending) - { - std::string priority = pending[i].getAttribute ("priority"); - if (priority == "M") - { - std::string project = pending[i].getAttribute ("project"); - if (countByProject[project] < limit && matching.find (i) == matching.end ()) - { - ++countByProject[project]; - matching[i] = true; - } - } - } - } - - // due:*, pri:L - for (unsigned int i = 0; i < pending.size (); ++i) - { - if (pending[i].getStatus () == T::pending) - { - std::string due = pending[i].getAttribute ("due"); - if (due != "") - { - std::string priority = pending[i].getAttribute ("priority"); - if (priority == "L") - { - std::string project = pending[i].getAttribute ("project"); - if (countByProject[project] < limit && matching.find (i) == matching.end ()) - { - ++countByProject[project]; - matching[i] = true; - } - } - } - } - } - - // pri:L - for (unsigned int i = 0; i < pending.size (); ++i) - { - if (pending[i].getStatus () == T::pending) - { - std::string priority = pending[i].getAttribute ("priority"); - if (priority == "L") - { - std::string project = pending[i].getAttribute ("project"); - if (countByProject[project] < limit && matching.find (i) == matching.end ()) - { - ++countByProject[project]; - matching[i] = true; - } - } - } - } - - // due:, pri: - for (unsigned int i = 0; i < pending.size (); ++i) - { - if (pending[i].getStatus () == T::pending) - { - std::string due = pending[i].getAttribute ("due"); - if (due == "") - { - std::string priority = pending[i].getAttribute ("priority"); - if (priority == "") - { - std::string project = pending[i].getAttribute ("project"); - if (countByProject[project] < limit && matching.find (i) == matching.end ()) - { - ++countByProject[project]; - matching[i] = true; - } - } - } - } - } - - // Convert map to vector. - foreach (i, matching) - all.push_back (i->first); -} - //////////////////////////////////////////////////////////////////////////////// void nag (const TDB& tdb, T& task, Config& conf) { @@ -3499,15 +392,19 @@ int getDueState (const std::string& due) //////////////////////////////////////////////////////////////////////////////// // Scan for recurring tasks, and generate any necessary instances of those // tasks. -void handleRecurrence (std::vector & tasks) +void handleRecurrence (const TDB& tdb, std::vector & tasks) { std::vector modified; + Date now; + std::cout << "# handleRecurrence" << std::endl; std::vector ::iterator it; for (it = tasks.begin (); it != tasks.end (); ++it) { if (it->getStatus () == T::recurring) { + std::cout << "# found recurring task " << it->getUUID () << std::endl; + // This task is recurring. While it remains hidden from view, it spawns // child tasks automatically, here, that are regular tasks, except they // have a "parent" attribute that contains the UUID of the original. @@ -3516,13 +413,71 @@ void handleRecurrence (std::vector & tasks) std::vector children; std::vector ::iterator them; for (them = tasks.begin (); them != tasks.end (); ++them) - if (them->getAttribute ("parent") != "") + if (them->getAttribute ("parent") == it->getUUID ()) children.push_back (*them); - // TODO Determine if any new child tasks need to be generated, and do it. + // Determine due date, recur period and until date. + Date due (atoi (it->getAttribute ("due").c_str ())); + std::cout << "# due=" << due.toString () << std::endl; + std::string recur = it->getAttribute ("recur"); + std::cout << "# recur=" << recur << std::endl; - // TODO if before "until" date, or "until" missing - // TODO Iterate from "due", incrementing by "recur" + bool specificEnd = false; + Date until; + if (it->getAttribute ("until") != "") + { + until = Date (atoi (it->getAttribute ("until").c_str ())); + specificEnd = true; + } + + std::cout << "# specficEnd=" << (specificEnd ? "true" : "false") << std::endl; + if (specificEnd) + std::cout << "# until=" << until.toString () << std::endl; + + for (Date i = due; ; i = getNextRecurrence (i, recur)) + { + std::cout << "# i=" << i.toString () << std::endl; + if (specificEnd && i > until) + break; + + // Look to see if there is a gap at date "i" by scanning children. + bool foundChild = false; + std::vector ::iterator cit; + for (cit = children.begin (); cit != children.end (); ++cit) + { + if (atoi (cit->getAttribute ("due").c_str ()) == i.toEpoch ()) + { + foundChild = true; + break; + } + } + +// TODO A gap may be filled by a completed task. Oh crap. + + // There is a gap, so insert a task. + if (!foundChild) + { + std::cout << "# found a gap at i=" << i.toString () << std::endl; + T rec (*it); // Clone the parent. + + char dueDate[16]; + sprintf (dueDate, "%u", (unsigned int) i.toEpoch ()); + rec.setAttribute ("due", dueDate); + rec.setAttribute ("parent", it->getUUID ()); + rec.setStatus (T::pending); + + std::cout << "# adding to modified" << std::endl; + modified.push_back (rec); + std::cout << "# adding to pending" << std::endl; + tdb.addT (rec); + } + + if (i > now) + { + std::cout << "# already 1 instance into the future, stopping" << std::endl; + break; + } + } } else modified.push_back (*it); @@ -3532,3 +487,41 @@ void handleRecurrence (std::vector & tasks) } //////////////////////////////////////////////////////////////////////////////// +Date getNextRecurrence (Date& current, std::string& period) +{ + int days = convertDuration (period); + + // Some periods are difficult, because they can be vague. + if (period == "monthly" || + (isdigit (period[0]) && period[period.length () - 1] == 'm')) + { + int m = current.month (); + int d = current.day (); + int y = current.year (); + + if (++m == 13) m = 1; + while (! Date::valid (m, d, y)) + --d; + + std::cout << "# next " << current.toString () << " + " << period << " = " << m << "/" << d << "/" << y << std::endl; + return Date (m, d, y); + } + + if (period == "bimonthly" || + period == "semimonthly" || + period == "quarterly" || + period == "biannual" || + period == "biyearly" || + period == "semiannual" || + (isdigit (period[0]) && ( + period[period.length () - 1] == 'm' || + period[period.length () - 1] == 'q'))) + { + // TODO lots of work here... + } + + // If the period is an 'easy' one, add it to current, and we're done. + return current + (days * 86400); +} + +//////////////////////////////////////////////////////////////////////////////// diff --git a/src/task.h b/src/task.h index 494689292..69ad75932 100644 --- a/src/task.h +++ b/src/task.h @@ -58,12 +58,29 @@ void parse (std::vector &, std::string&, T&, Config&); bool validDate (std::string&, Config&); // task.cpp +void gatherNextTasks (const TDB&, T&, Config&, std::vector &, std::vector &); +void nag (const TDB&, T&, Config&); +int getDueState (const std::string&); +void handleRecurrence (const TDB&, std::vector &); +Date getNextRecurrence (Date&, std::string&); + +// command.cpp void handleAdd (const TDB&, T&, Config&); void handleProjects (const TDB&, T&, Config&); void handleTags (const TDB&, T&, Config&); +void handleUndelete (const TDB&, T&, Config&); +void handleVersion (Config&); +void handleExport (const TDB&, T&, Config&); +void handleDelete (const TDB&, T&, Config&); +void handleStart (const TDB&, T&, Config&); +void handleDone (const TDB&, T&, Config&); +void handleModify (const TDB&, T&, Config&); +void handleColor (Config&); + +// report.cpp +void filter (std::vector&, T&); void handleList (const TDB&, T&, Config&); void handleInfo (const TDB&, T&, Config&); -void handleUndelete (const TDB&, T&, Config&); void handleLongList (const TDB&, T&, Config&); void handleSmallList (const TDB&, T&, Config&); void handleCompleted (const TDB&, T&, Config&); @@ -78,16 +95,6 @@ void handleReportOverdue (const TDB&, T&, Config&); void handleReportStats (const TDB&, T&, Config&); void handleReportOldest (const TDB&, T&, Config&); void handleReportNewest (const TDB&, T&, Config&); -void handleVersion (Config&); -void handleExport (const TDB&, T&, Config&); -void handleDelete (const TDB&, T&, Config&); -void handleStart (const TDB&, T&, Config&); -void handleDone (const TDB&, T&, Config&); -void handleModify (const TDB&, T&, Config&); -void handleColor (Config&); -void gatherNextTasks (const TDB&, T&, Config&, std::vector &, std::vector &); -void nag (const TDB&, T&, Config&); -void handleRecurrence (std::vector &); // util.cpp bool confirm (const std::string&); @@ -109,9 +116,7 @@ void formatTimeDeltaDays (std::string&, time_t); std::string formatSeconds (time_t); const std::string uuid (); const char* optionalBlankLine (Config&); -int convertDuration (const std::string&); -int getDueState (const std::string&); -int addDuration (const Date&, const std::string&); +int convertDuration (std::string&); // rules.cpp void initializeColorRules (Config&); diff --git a/src/util.cpp b/src/util.cpp index 877f4791a..edc02b62d 100644 --- a/src/util.cpp +++ b/src/util.cpp @@ -238,9 +238,9 @@ const std::string uuid () //////////////////////////////////////////////////////////////////////////////// // Recognize the following constructs, and return the number of days represented -int convertDuration (const std::string& input) +int convertDuration (std::string& input) { - std::string in (lowerCase (input)); + input = lowerCase (input); Date today; std::vector supported; @@ -261,7 +261,7 @@ int convertDuration (const std::string& input) supported.push_back ("yearly"); std::vector matches; - if (autoComplete (in, supported, matches) == 1) + if (autoComplete (input, supported, matches) == 1) { std::string found = matches[0]; @@ -306,9 +306,3 @@ int convertDuration (const std::string& input) } //////////////////////////////////////////////////////////////////////////////// -int addDuration (const Date& base, const std::string& offset) -{ - return 0; -} - -////////////////////////////////////////////////////////////////////////////////