diff --git a/Date.cpp b/Date.cpp new file mode 100644 index 000000000..9bd67a4c8 --- /dev/null +++ b/Date.cpp @@ -0,0 +1,304 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright 2005 - 2008, Paul Beckingham. All rights reserved. +// +// +//////////////////////////////////////////////////////////////////////////////// +#include +#include +#include +#include "task.h" +#include "Date.h" + +//////////////////////////////////////////////////////////////////////////////// +// Defaults to "now". +Date::Date () +{ + mT = time (NULL); +} + +//////////////////////////////////////////////////////////////////////////////// +Date::Date (const time_t t) +{ + mT = t; +} + +//////////////////////////////////////////////////////////////////////////////// +Date::Date (const int m, const int d, const int y) +{ + // Error if not valid. + struct tm t = {0}; + t.tm_mday = d; + t.tm_mon = m - 1; + t.tm_year = y - 1900; + + mT = mktime (&t); +} + +//////////////////////////////////////////////////////////////////////////////// +Date::Date (const std::string& mdy) +{ + size_t firstSlash = mdy.find ("/"); + size_t secondSlash = mdy.find ("/", firstSlash + 1); + if (firstSlash != std::string::npos && + secondSlash != std::string::npos) + { + int m = ::atoi (mdy.substr (0, firstSlash ).c_str ()); + int d = ::atoi (mdy.substr (firstSlash + 1, secondSlash - firstSlash).c_str ()); + int y = ::atoi (mdy.substr (secondSlash + 1, std::string::npos ).c_str ()); + if (!valid (m, d, y)) + throw std::string ("\"") + mdy + "\" is not a valid date."; + + // Duplicate Date::Date (const int, const int, const int); + struct tm t = {0}; + t.tm_mday = d; + t.tm_mon = m - 1; + t.tm_year = y - 1900; + + mT = mktime (&t); + } + else + throw std::string ("\"") + mdy + "\" is not a valid date."; +} + +//////////////////////////////////////////////////////////////////////////////// +Date::Date (const Date& rhs) +{ + mT = rhs.mT; +} + +//////////////////////////////////////////////////////////////////////////////// +Date::~Date () +{ +} + +//////////////////////////////////////////////////////////////////////////////// +time_t Date::toEpoch () +{ + return mT; +} + +//////////////////////////////////////////////////////////////////////////////// +void Date::toEpoch (time_t& epoch) +{ + epoch = mT; +} + +//////////////////////////////////////////////////////////////////////////////// +void Date::toMDY (int& m, int& d, int& y) +{ + struct tm* t = localtime (&mT); + + m = t->tm_mon + 1; + d = t->tm_mday; + y = t->tm_year + 1900; +} + +//////////////////////////////////////////////////////////////////////////////// +void Date::toString (std::string& output) +{ + output = toString (); +} + +//////////////////////////////////////////////////////////////////////////////// +std::string Date::toString (void) +{ + int m, d, y; + toMDY (m, d, y); + + char formatted [11]; + sprintf (formatted, "%d/%d/%d", m, d, y); + return std::string (formatted); +} + +//////////////////////////////////////////////////////////////////////////////// +bool Date::valid (const int m, const int d, const int y) +{ + // Check that the year is valid. + if (y < 0) + return false; + + // Check that the month is valid. + if (m < 1 || m > 12) + return false; + + // Finally check that the days fall within the acceptable range for this + // month, and whether or not this is a leap year. + if (d < 1 || d > Date::daysInMonth (m, y)) + return false; + + return true; +} + +//////////////////////////////////////////////////////////////////////////////// +bool Date::leapYear (int year) +{ + bool ly = false; + + if (!(year % 4)) ly = true; + else if (!(year % 400)) ly = true; + else if (!(year % 100)) ly = false; + + return ly; +} + +//////////////////////////////////////////////////////////////////////////////// +int Date::daysInMonth (int month, int year) +{ + static int days[2][12] = + { + {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}, + {31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31} + }; + + return days[Date::leapYear (year) ? 1 : 0][month - 1]; +} + +//////////////////////////////////////////////////////////////////////////////// +std::string Date::monthName (int month) +{ + static const char* months[12] = + { + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", + }; + + assert (month > 0); + assert (month <= 12); + return months[month -1]; +} + +//////////////////////////////////////////////////////////////////////////////// +void Date::dayName (int dow, std::string& name) +{ + static const char* days[7] = + { + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + }; + + name = days[dow]; +} + +//////////////////////////////////////////////////////////////////////////////// +std::string Date::dayName (int dow) +{ + static const char* days[7] = + { + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + }; + + return days[dow]; +} + +//////////////////////////////////////////////////////////////////////////////// +int Date::dayOfWeek () +{ + struct tm* t = localtime (&mT); + return t->tm_wday; +} + +//////////////////////////////////////////////////////////////////////////////// +int Date::month () +{ + struct tm* t = localtime (&mT); + return t->tm_mon + 1; +} + +//////////////////////////////////////////////////////////////////////////////// +int Date::day () +{ + struct tm* t = localtime (&mT); + return t->tm_mday; +} + +//////////////////////////////////////////////////////////////////////////////// +int Date::year () +{ + struct tm* t = localtime (&mT); + return t->tm_year + 1900; +} + +//////////////////////////////////////////////////////////////////////////////// +bool Date::operator== (const Date& rhs) +{ + return rhs.mT == mT; +} + +//////////////////////////////////////////////////////////////////////////////// +bool Date::operator!= (const Date& rhs) +{ + return rhs.mT != mT; +} + +//////////////////////////////////////////////////////////////////////////////// +bool Date::operator< (const Date& rhs) +{ + return mT < rhs.mT; +} + +//////////////////////////////////////////////////////////////////////////////// +bool Date::operator> (const Date& rhs) +{ + return mT > rhs.mT; +} + +//////////////////////////////////////////////////////////////////////////////// +bool Date::operator<= (const Date& rhs) +{ + return mT <= rhs.mT; +} + +//////////////////////////////////////////////////////////////////////////////// +bool Date::operator>= (const Date& rhs) +{ + return mT >= rhs.mT; +} + +//////////////////////////////////////////////////////////////////////////////// +Date Date::operator+ (const int delta) +{ + return Date::Date (mT + delta); +} + +//////////////////////////////////////////////////////////////////////////////// +Date& Date::operator+= (const int delta) +{ + mT += (time_t) delta; + return *this; +} + +//////////////////////////////////////////////////////////////////////////////// +Date& Date::operator-= (const int delta) +{ + mT -= (time_t) delta; + return *this; +} + +//////////////////////////////////////////////////////////////////////////////// +time_t Date::operator- (const Date& rhs) +{ + return mT - rhs.mT; +} + +//////////////////////////////////////////////////////////////////////////////// diff --git a/parse.cpp b/parse.cpp new file mode 100644 index 000000000..71b9e8481 --- /dev/null +++ b/parse.cpp @@ -0,0 +1,379 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright 2006 - 2008, Paul Beckingham. All rights reserved. +// +// +//////////////////////////////////////////////////////////////////////////////// +#include +#include +#include +#include + +#include "Date.h" +#include "task.h" +#include "T.h" + +//////////////////////////////////////////////////////////////////////////////// +static const char* colors[] = +{ + "bold", + "underline", + "bold_underline", + + "black", + "red", + "green", + "yellow", + "blue", + "magenta", + "cyan", + "white", + + "bold_black", + "bold_red", + "bold_green", + "bold_yellow", + "bold_blue", + "bold_magenta", + "bold_cyan", + "bold_white", + + "underline_black", + "underline_red", + "underline_green", + "underline_yellow", + "underline_blue", + "underline_magenta", + "underline_cyan", + "underline_white", + + "bold_underline_black", + "bold_underline_red", + "bold_underline_green", + "bold_underline_yellow", + "bold_underline_blue", + "bold_underline_magenta", + "bold_underline_cyan", + "bold_underline_white", + + "on_black", + "on_red", + "on_green", + "on_yellow", + "on_blue", + "on_magenta", + "on_cyan", + "on_white", + + "on_bright_black", + "on_bright_red", + "on_bright_green", + "on_bright_yellow", + "on_bright_blue", + "on_bright_magenta", + "on_bright_cyan", + "on_bright_white", + "", +}; + +static const char* attributes[] = +{ + "project", + "priority", + "fg", + "bg", + "due", + "entry", + "start", + "end", + "", +}; + +static const char* commands[] = +{ + "active", + "add", + "calendar", + "colors", + "completed", + "delete", + "done", + "export", + "history", + "info", + "list", + "long", + "ls", + "next", + "overdue", + "projects", + "start", + "stats", + "summary", + "tags", + "usage", + "version", + "", +}; + +void guess (const std::string& type, const char** list, std::string& candidate) +{ + std::vector options; + for (int i = 0; list[i][0]; ++i) + options.push_back (list[i]); + + std::vector matches; + autoComplete (candidate, options, matches); + if (1 == matches.size ()) + candidate = matches[0]; + + else if (0 == matches.size ()) + throw std::string ("Unrecognized ") + type + " '" + candidate + "'"; + + else + { + std::string error = "Ambiguous "; + error += type; + error += " '"; + error += candidate; + error += "' - could be either of "; + for (size_t i = 0; i < matches.size (); ++i) + { + if (i) + error += ", "; + error += matches[i]; + } + + throw error; + } +} + +//////////////////////////////////////////////////////////////////////////////// +static bool isCommand (const std::string& candidate) +{ + std::vector options; + for (int i = 0; commands[i][0]; ++i) + options.push_back (commands[i]); + + std::vector matches; + autoComplete (candidate, options, matches); + if (0 == matches.size ()) + return false; + + return true; +} + +//////////////////////////////////////////////////////////////////////////////// +bool validDate (std::string& date) +{ + size_t firstSlash = date.find ("/"); + size_t secondSlash = date.find ("/", firstSlash + 1); + if (firstSlash != std::string::npos && + secondSlash != std::string::npos) + { + int m = ::atoi (date.substr (0, firstSlash ).c_str ()); + int d = ::atoi (date.substr (firstSlash + 1, secondSlash - firstSlash).c_str ()); + int y = ::atoi (date.substr (secondSlash + 1, std::string::npos ).c_str ()); + if (!Date::valid (m, d, y)) + throw std::string ("\"") + date + "\" is not a valid date."; + + // Convert to epoch form. + Date dt (m, d, y); + time_t t; + dt.toEpoch (t); + char converted[12]; + sprintf (converted, "%u", (unsigned int) t); + date = converted; + } + else + throw std::string ("Badly formed date - use the MM/DD/YYYY format"); + + return true; +} + +//////////////////////////////////////////////////////////////////////////////// +static bool validPriority (std::string& input) +{ + if (input != "H" && + input != "M" && + input != "L" && + input != "") + throw std::string ("\"") + + input + + "\" is not a valid priority. Use H, M, L or leave blank."; + + return true; +} + +//////////////////////////////////////////////////////////////////////////////// +static bool validAttribute (std::string& name, std::string& value) +{ + guess ("attribute", attributes, name); + + if ((name == "fg" || name == "bg") && value != "") + guess ("color", colors, value); + + else if (name == "due" && value != "") + validDate (value); + + else if (name == "priority") + { + for (std::string::iterator i = value.begin (); i != value.end (); ++i) + *i = ::toupper (*i); + + return validPriority (value); + } + + else if (name == "entry" || + name == "start" || + name == "end") + throw std::string ("\"") + + name + + "\" is not an attribute you may modify directly."; + + return true; +} + +//////////////////////////////////////////////////////////////////////////////// +static bool validId (const std::string& input) +{ + for (size_t i = 0; i < input.length (); ++i) + if (!::isdigit (input[i])) + return false; + + return true; +} + +//////////////////////////////////////////////////////////////////////////////// +static bool validTag (std::string& input) +{ + if ((input[0] == '-' || input[0] == '+') && + input.length () > 1) + return true; + + return false; +} + +//////////////////////////////////////////////////////////////////////////////// +static bool validDescription (const std::string& input) +{ + if (input.length () > 0) + return true; + + return false; +} + +//////////////////////////////////////////////////////////////////////////////// +static bool validCommand (std::string& input) +{ + guess ("command", commands, input); + return true; +} + +//////////////////////////////////////////////////////////////////////////////// +static bool validSubstitution ( + std::string& input, + std::string& from, + std::string& to) +{ + size_t first = input.find ('/'); + if (first != std::string::npos) + { + size_t second = input.find ('/', first + 1); + if (second != std::string::npos) + { + size_t third = input.find ('/', second + 1); + if (third != std::string::npos) + { + if (first == 0 && + first < second && + second < third && + third == input.length () - 1) + { + from = input.substr (first + 1, second - first - 1); + to = input.substr (second + 1, third - second - 1); + return true; + } + } + } + } + + return false; +} + +//////////////////////////////////////////////////////////////////////////////// +// Token Distinguishing characteristic +// ------- ----------------------------- +// command first positional +// id \d+ +// description default, accumulate +// substitution /\w+/\w*/ +// tags [-+]\w+ +// attributes \w+:.+ +// +void parse ( + std::vector & args, + std::string& command, + T& task) +{ + command = ""; + + std::string descCandidate = ""; + for (size_t i = 0; i < args.size (); ++i) + { + std::string arg (args[i]); + size_t colon; // Pointer to colon in argument. + std::string from; + std::string to; + + // An id is the first argument found that contains all digits. + if (command != "add" && // "add" doesn't require an ID + task.getId () == 0 && + validId (arg)) + task.setId (::atoi (arg.c_str ())); + + // Tags begin with + or - and contain arbitrary text. + else if (validTag (arg)) + { + if (arg[0] == '+') + task.addTag (arg.substr (1, std::string::npos)); + else if (arg[0] == '-') + task.addRemoveTag (arg.substr (1, std::string::npos)); + } + + // Attributes contain a constant string followed by a colon, followed by a + // value. + else if ((colon = arg.find (":")) != std::string::npos) + { + std::string name = arg.substr (0, colon); + std::string value = arg.substr (colon + 1, std::string::npos); + + if (validAttribute (name, value)) + task.setAttribute (name, value); + } + + // Substitution of description text. + else if (validSubstitution (arg, from, to)) + { + task.setSubstitution (from, to); + } + + // Command. + else if (command == "") + { + if (!isCommand (arg)) + descCandidate += std::string (arg) + " "; + else if (validCommand (arg)) + command = arg; + } + + // Anything else is just considered description. + else + descCandidate += std::string (arg) + " "; + } + + if (validDescription (descCandidate)) + task.setDescription (descCandidate); +} + +//////////////////////////////////////////////////////////////////////////////// + diff --git a/task.cpp b/task.cpp new file mode 100644 index 000000000..38d60fe57 --- /dev/null +++ b/task.cpp @@ -0,0 +1,2760 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright 2006 - 2008, Paul Beckingham. +// All rights reserved. +// +// +//////////////////////////////////////////////////////////////////////////////// +#include +#include +#include +#include +#include +#include +#include + +#include "Config.h" +#include "Date.h" +#include "Table.h" +#include "stlmacros.h" +#include "TDB.h" +#include "T.h" +#include "task.h" + +#ifdef HAVE_LIBNCURSES +#include +#endif + +//////////////////////////////////////////////////////////////////////////////// +void usage (Config& conf) +{ + Table table; + int width = 80; +#ifdef HAVE_LIBNCURSES + if (conf.get ("curses", true)) + { + WINDOW* w = initscr (); + width = w->_maxx + 1; + endwin (); + } +#endif + + table.addColumn (" "); + table.addColumn (" "); + table.addColumn (" "); + + table.setColumnJustification (0, Table::left); + table.setColumnJustification (1, Table::left); + table.setColumnJustification (2, Table::left); + + table.setColumnWidth (0, Table::minimum); + table.setColumnWidth (1, Table::minimum); + table.setColumnWidth (2, Table::flexible); + table.setTableWidth (width); + + int row = table.addRow (); + table.addCell (row, 0, "Usage:"); + table.addCell (row, 1, "task"); + + row = table.addRow (); + table.addCell (row, 1, "task add [tags] [attrs] desc..."); + table.addCell (row, 2, "Adds a new task"); + + row = table.addRow (); + table.addCell (row, 1, "task list [tags] [attrs] desc..."); + table.addCell (row, 2, "Lists all tasks matching the specified criteria"); + + row = table.addRow (); + table.addCell (row, 1, "task long [tags] [attrs] desc..."); + table.addCell (row, 2, "Lists all task, all data, matching the specified criteria"); + + row = table.addRow (); + table.addCell (row, 1, "task ls [tags] [attrs] desc..."); + table.addCell (row, 2, "Minimal listing of all tasks matching the specified criteria"); + + row = table.addRow (); + table.addCell (row, 1, "task completed [tags] [attrs] desc..."); + table.addCell (row, 2, "Chronological listing of all completed tasks matching the specified criteria"); + + row = table.addRow (); + table.addCell (row, 1, "task ID [tags] [attrs] [desc...]"); + table.addCell (row, 2, "Modifies the existing task with provided arguments"); + + row = table.addRow (); + table.addCell (row, 1, "task ID /from/to/"); + table.addCell (row, 2, "Perform the substitution on the desc, for fixing mistakes"); + + row = table.addRow (); + table.addCell (row, 1, "task delete ID"); + table.addCell (row, 2, "Deletes the specified task"); + + row = table.addRow (); + table.addCell (row, 1, "task info ID"); + table.addCell (row, 2, "Shows all data, metadata for specified task"); + + row = table.addRow (); + table.addCell (row, 1, "task start ID"); + table.addCell (row, 2, "Marks specified task as started, starts the clock ticking"); + + row = table.addRow (); + table.addCell (row, 1, "task done ID"); + table.addCell (row, 2, "Marks the specified task as completed"); + + row = table.addRow (); + table.addCell (row, 1, "task projects"); + table.addCell (row, 2, "Shows a list of all project names used, and how many tasks are in each"); + + row = table.addRow (); + table.addCell (row, 1, "task tags"); + table.addCell (row, 2, "Shows a list of all tags used"); + + row = table.addRow (); + table.addCell (row, 1, "task summary"); + table.addCell (row, 2, "Shows a report of task status by project"); + + row = table.addRow (); + table.addCell (row, 1, "task history"); + table.addCell (row, 2, "Shows a report of task history, by month"); + + row = table.addRow (); + table.addCell (row, 1, "task next"); + table.addCell (row, 2, "Shows the most important tasks for each project"); + + row = table.addRow (); + table.addCell (row, 1, "task calendar"); + table.addCell (row, 2, "Shows a monthly calendar, with due tasks marked"); + + row = table.addRow (); + table.addCell (row, 1, "task active"); + table.addCell (row, 2, "Shows all task that are started, but not completed"); + + row = table.addRow (); + table.addCell (row, 1, "task overdue"); + table.addCell (row, 2, "Shows all incomplete tasks that are beyond their due date"); + + row = table.addRow (); + table.addCell (row, 1, "task stats"); + table.addCell (row, 2, "Shows task database statistics"); + + row = table.addRow (); + table.addCell (row, 1, "task usage"); + table.addCell (row, 2, "Shows task command usage frequency"); + + row = table.addRow (); + table.addCell (row, 1, "task export"); + table.addCell (row, 2, "Exports all tasks as a CSV file"); + + row = table.addRow (); + table.addCell (row, 1, "task color"); + table.addCell (row, 2, "Displays all possible colors"); + + row = table.addRow (); + table.addCell (row, 1, "task version"); + table.addCell (row, 2, "Shows the task version number"); + + std::cout << table.render () + << std::endl; + + std::cout + << "ID is the numeric identifier displayed by the 'task list' command" << "\n" + << "\n" + << "Tags are arbitrary words, any quantity:" << "\n" + << " +tag The + means add the tag" << "\n" + << " -tag The - means remove the tag" << "\n" + << "\n" + << "Attributes are:" << "\n" + << " project: Project name" << "\n" + << " priority: Priority" << "\n" + << " due: Due date" << "\n" + << " fg: Foreground color" << "\n" + << " bg: Background color" << "\n" + << "\n" + << "Any command or attribute name may be abbreviated if still unique:" << "\n" + << " task list project:Home" << "\n" + << " task li pro:Home" << "\n" + << "\n" + << "Some task descriptions need to be escaped because of the shell:" << "\n" + << " task add \"quoted ' quote\"" << "\n" + << " task add escaped \\' quote" << "\n" + << "\n" + << "Many characters have special meaning to the shell, including:" << "\n" + << " $ ! ' \" ( ) ; \\ ` * ? { } [ ] < > | & % # ~" << "\n" + << std::endl; +} + +//////////////////////////////////////////////////////////////////////////////// +int main (int argc, char** argv) +{ +// TODO Find out what this is, and either promote it to live code, or remove it. +// std::set_terminate (__gnu_cxx::__verbose_terminate_handler); + + try + { + // Load the config file from the home directory. If the file cannot be + // found, offer to create a sample one. + Config conf; + struct passwd* pw = getpwuid (getuid ()); + if (!pw) + throw std::string ("Could not read home directory from passwd file."); + + std::string home = pw->pw_dir; + home += "/.taskrc"; + if (!conf.load (home)) + conf.createDefault (home); + + TDB tdb; + tdb.dataDirectory (conf.get ("data.location")); + + // Log commands, if desired. + if (conf.get ("command.logging") == "on") + tdb.logCommand (argc, argv); + + // Parse the command line. + std::vector args; + for (int i = 1; i < argc; ++i) + args.push_back (argv[i]); + + std::string command; + T task; + parse (args, command, task); + + if (command == "add") handleAdd (tdb, task, conf); + else if (command == "projects") handleProjects (tdb, task, conf); + else if (command == "tags") handleTags (tdb, task, conf); + else if (command == "list") handleList (tdb, task, conf); + else if (command == "info") handleInfo (tdb, task, conf); + else if (command == "long") handleLongList (tdb, task, conf); + else if (command == "ls") handleSmallList (tdb, task, conf); + else if (command == "colors") handleColor ( conf); + else if (command == "completed") handleCompleted (tdb, task, conf); + else if (command == "delete") handleDelete (tdb, task, conf); + else if (command == "start") handleStart (tdb, task, conf); + else if (command == "done") handleDone (tdb, task, conf); + else if (command == "export") handleExport (tdb, task, conf); + else if (command == "version") handleVersion ( conf); + else if (command == "summary") handleReportSummary (tdb, task, conf); + else if (command == "next") handleReportNext (tdb, task, conf); + else if (command == "history") handleReportHistory (tdb, task, conf); + else if (command == "calendar") handleReportCalendar (tdb, task, conf); + else if (command == "active") handleReportActive (tdb, task, conf); + else if (command == "overdue") handleReportOverdue (tdb, task, conf); + else if (command == "stats") handleReportStats (tdb, task, conf); + else if (command == "usage") handleReportUsage (tdb, task, conf); + else if (command == "" && task.getId ()) handleModify (tdb, task, conf); + else usage (conf); + } + + catch (std::string& error) + { + std::cout << error << std::endl; + return -1; + } + + catch (...) + { + std::cout << "Unknown error." << std::endl; + return -2; + } + +// return 0; + exit (0); +} + +//////////////////////////////////////////////////////////////////////////////// +std::string epochToString (const std::string& epoch) +{ + char formatted[12] = {0}; + + if (epoch.length () && epoch.find ("/") == std::string::npos) + { + Date dt (::atoi (epoch.c_str ())); + int m, d, y; + dt.toMDY (m, d, y); + sprintf (formatted, "%d/%d/%04d", m, d, y); + } + + return formatted; +} + +//////////////////////////////////////////////////////////////////////////////// +void handleAdd (const TDB& tdb, T& task, Config& conf) +{ + char entryTime[16]; + sprintf (entryTime, "%u", (unsigned int) time (NULL)); + task.setAttribute ("entry", entryTime); + + if (task.getDescription () == "") +// std::cout << "Cannot add a blank task" << std::endl; + 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; + } + + // Render a list of project names from the map. + Table table; + table.addColumn ("Project"); + table.addColumn ("Tasks"); + + table.setColumnUnderline (0); + table.setColumnUnderline (1); + + table.setColumnJustification (1, Table::right); + + foreach (i, unique) + { + int row = table.addRow (); + table.addCell (row, 0, i->first); + table.addCell (row, 1, i->second); + } + + std::cout << std::endl + << table.render () + << std::endl + << unique.size () + << (unique.size () == 1 ? " project" : " 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. + foreach (i, unique) + std::cout << i->first << std::endl; + + if (unique.size ()) + std::cout << std::endl + << 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 = 80; +#ifdef HAVE_LIBNCURSES + if (conf.get ("curses", true)) + { + WINDOW* w = initscr (); + width = w->_maxx + 1; + endwin (); + } +#endif + + /* int count = */ tdb.gc (); + + // Get the pending tasks. + std::vector tasks; + tdb.pendingT (tasks); + + initializeColorRules (conf); + + // 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"); + table.addColumn ("Age"); + table.addColumn ("Description"); + + table.setColumnUnderline (0); + table.setColumnUnderline (1); + table.setColumnUnderline (2); + table.setColumnUnderline (3); + table.setColumnUnderline (4); + table.setColumnUnderline (5); + 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); + table.setColumnWidth (5, Table::minimum); + table.setColumnWidth (6, Table::flexible); + + table.setColumnJustification (0, Table::right); + table.setColumnJustification (3, Table::right); + table.setColumnJustification (5, Table::right); + + table.sortOn (3, Table::ascendingDate); + table.sortOn (2, Table::descendingPriority); + table.sortOn (1, Table::ascendingCharacter); + + // Split any description specified into words. + std::vector descWords; + split (descWords, 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 < tasks.size (); ++i) + { + T refTask (tasks[i]); + + // Apply description filter. + unsigned int matches = 0; + for (unsigned int w = 0; w < descWords.size (); ++w) + if (refTask.getDescription ().find (descWords[w]) != std::string::npos) + ++matches; + + if (matches == descWords.size ()) + { + // Apply attribute filter. + matches = 0; + foreach (a, attrList) + 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 ()) + { + // Now format the matching task. + bool imminent = false; + bool overdue = false; + Date now; + std::string due = refTask.getAttribute ("due"); + if (due.length () && due.find ("/") == std::string::npos) + { + Date dt (::atoi (due.c_str ())); + int m, d, y; + dt.toMDY (m, d, y); + char formatted[12]; + sprintf (formatted, "%d/%d/%04d", m, d, y); + due = formatted; + + overdue = (dt < now) ? true : false; + now += 7 * 86400; + imminent = dt < now ? true : false; + } + + std::string active; + if (refTask.getAttribute ("start") != "") + active = "*"; + + std::string age; + std::string created = refTask.getAttribute ("entry"); + if (created.length () && created.find ("/") == std::string::npos) + { + 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); + table.addCell (row, 5, age); + table.addCell (row, 6, 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 << std::endl + << table.render () + << std::endl + << table.rowCount () + << (table.rowCount () == 1 ? " task" : " tasks") + << std::endl; + else + std::cout << "No matches." + << std::endl; + +/* + if (count) + std::cout << std::endl + << "[gc: " + << count + << " transferred]" + << 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 = 80; +#ifdef HAVE_LIBNCURSES + if (conf.get ("curses", true)) + { + WINDOW* w = initscr (); + width = w->_maxx + 1; + endwin (); + } +#endif + + /* int count = */ tdb.gc (); + + // Get the pending tasks. + std::vector tasks; + tdb.pendingT (tasks); + + initializeColorRules (conf); + + // Create a table for output. + Table table; + table.setTableWidth (width); + table.addColumn ("ID"); + table.addColumn ("Project"); + table.addColumn ("Pri"); + table.addColumn ("Description"); + + 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); + + // Split any description specified into words. + std::vector descWords; + split (descWords, 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 < tasks.size (); ++i) + { + T refTask (tasks[i]); + + // Apply description filter. + unsigned int matches = 0; + for (unsigned int w = 0; w < descWords.size (); ++w) + if (refTask.getDescription ().find (descWords[w]) != std::string::npos) + ++matches; + + if (matches == descWords.size ()) + { + // Apply attribute filter. + matches = 0; + foreach (a, attrList) + 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 ()) + { + // Now format the matching task. + bool imminent = false; + bool overdue = false; + Date now; + std::string due = refTask.getAttribute ("due"); + if (due.length () && due.find ("/") == std::string::npos) + { + Date dt (::atoi (due.c_str ())); + int m, d, y; + dt.toMDY (m, d, y); + char formatted[12]; + sprintf (formatted, "%d/%d/%04d", m, d, y); + due = formatted; + + overdue = (dt < now) ? true : false; + now += 7 * 86400; + imminent = dt < now ? true : false; + } + + std::string active; + if (refTask.getAttribute ("start") != "") + active = "*"; + + std::string age; + std::string created = refTask.getAttribute ("entry"); + if (created.length () && created.find ("/") == std::string::npos) + { + 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 << std::endl + << table.render () + << std::endl + << table.rowCount () + << (table.rowCount () == 1 ? " task" : " tasks") + << std::endl; + else + std::cout << "No matches." + << std::endl; + +/* + if (count) + std::cout << std::endl + << "[gc: " + << count + << " transferred]" + << 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 = 80; +#ifdef HAVE_LIBNCURSES + if (conf.get ("curses", true)) + { + WINDOW* w = initscr (); + width = w->_maxx + 1; + endwin (); + } +#endif + + /* int count = */ tdb.gc (); + + // Get the pending tasks. + std::vector tasks; + tdb.completedT (tasks); + + initializeColorRules (conf); + + // Create a table for output. + Table table; + table.setTableWidth (width); + table.addColumn ("Done"); + table.addColumn ("Project"); + table.addColumn ("Description"); + + 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); + + // Split any description specified into words. + std::vector descWords; + split (descWords, 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 < tasks.size (); ++i) + { + T refTask (tasks[i]); + + // Apply description filter. + unsigned int matches = 0; + for (unsigned int w = 0; w < descWords.size (); ++w) + if (refTask.getDescription ().find (descWords[w]) != std::string::npos) + ++matches; + + if (matches == descWords.size ()) + { + // Apply attribute filter. + matches = 0; + foreach (a, attrList) + 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 ()) + { + // 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 ()); + 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 << std::endl + << table.render () + << std::endl + << table.rowCount () + << (table.rowCount () == 1 ? " task" : " tasks") + << std::endl; + else + std::cout << "No matches." + << std::endl; + +/* + if (count) + std::cout << std::endl + << "[gc: " + << count + << " transferred]" + << 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 = 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.addColumn ("Name"); + table.addColumn ("Value"); + + 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" + : "")); + + 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")); + } + + // due (colored) + bool imminent = false; + bool overdue = false; + std::string due = refTask.getAttribute ("due"); + if (due != "") + { + row = table.addRow (); + table.addCell (row, 0, "Due"); + table.addCell (row, 1, epochToString (due)); + + if (due.length () && due.find ("/") == std::string::npos) + { + Date dt (::atoi (due.c_str ())); + + overdue = (dt < now) ? true : false; + now += 7 * 86400; + imminent = dt < now ? 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"); + table.addCell (row, 1, epochToString (refTask.getAttribute ("start"))); + } + + // end + if (refTask.getAttribute ("end") != "") + { + row = table.addRow (); + table.addCell (row, 0, "End"); + table.addCell (row, 1, epochToString (refTask.getAttribute ("end"))); + } + + // 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"); + std::string entry = epochToString (refTask.getAttribute ("entry")); + + std::string age; + std::string created = refTask.getAttribute ("entry"); + if (created.length () && created.find ("/") == std::string::npos) + { + Date dt (::atoi (created.c_str ())); + formatTimeDeltaDays (age, (time_t) (now - dt)); + } + + table.addCell (row, 1, entry + " (" + age + ")"); + } + } + + if (table.rowCount ()) + std::cout << std::endl + << table.render () + << std::endl + << 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 = 80; +#ifdef HAVE_LIBNCURSES + if (conf.get ("curses", true)) + { + WINDOW* w = initscr (); + width = w->_maxx + 1; + endwin (); + } +#endif + + /* int count = */ tdb.gc (); + + // Get all the tasks. + std::vector tasks; + tdb.pendingT (tasks); + + initializeColorRules (conf); + + // Create a table for output. + Table table; + table.setTableWidth (width); + table.addColumn ("ID"); + table.addColumn ("Project"); + table.addColumn ("Pri"); + table.addColumn ("Entry"); + table.addColumn ("Start"); + table.addColumn ("Due"); + table.addColumn ("Age"); + table.addColumn ("Description"); + + table.setColumnUnderline (0); + table.setColumnUnderline (1); + table.setColumnUnderline (2); + table.setColumnUnderline (3); + table.setColumnUnderline (4); + table.setColumnUnderline (5); + table.setColumnUnderline (6); + table.setColumnUnderline (7); + + 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); + table.setColumnWidth (6, Table::minimum); + table.setColumnWidth (7, Table::flexible); + + table.setColumnJustification (0, Table::right); + table.setColumnJustification (3, Table::right); + table.setColumnJustification (4, Table::right); + table.setColumnJustification (5, Table::right); + table.setColumnJustification (6, Table::right); + + table.sortOn (5, Table::ascendingDate); + table.sortOn (2, Table::descendingPriority); + table.sortOn (1, Table::ascendingCharacter); + + // Split any description specified into words. + std::vector descWords; + split (descWords, 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 < tasks.size (); ++i) + { + T refTask (tasks[i]); + + // Apply description filter. + unsigned int matches = 0; + for (unsigned int w = 0; w < descWords.size (); ++w) + if (refTask.getDescription ().find (descWords[w]) != std::string::npos) + ++matches; + + if (matches == descWords.size ()) + { + // Apply attribute filter. + matches = 0; + foreach (a, attrList) + 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 ()) + { + Date now; + + std::string started = refTask.getAttribute ("start"); + if (started.length () && started.find ("/") == std::string::npos) + { + Date dt (::atoi (started.c_str ())); + int m, d, y; + dt.toMDY (m, d, y); + char formatted[12]; + sprintf (formatted, "%d/%d/%04d", m, d, y); + started = formatted; + } + + std::string entered = refTask.getAttribute ("entry"); + if (entered.length () && entered.find ("/") == std::string::npos) + { + Date dt (::atoi (entered.c_str ())); + int m, d, y; + dt.toMDY (m, d, y); + char formatted[12]; + sprintf (formatted, "%d/%d/%04d", m, d, y); + entered = formatted; + } + + // Now format the matching task. + bool imminent = false; + bool overdue = false; + std::string due = refTask.getAttribute ("due"); + if (due.length () && due.find ("/") == std::string::npos) + { + Date dt (::atoi (due.c_str ())); + int m, d, y; + dt.toMDY (m, d, y); + char formatted[12]; + sprintf (formatted, "%d/%d/%04d", m, d, y); + due = formatted; + + overdue = (dt < now) ? true : false; + now += 7 * 86400; + imminent = dt < now ? true : false; + } + + std::string age; + std::string created = refTask.getAttribute ("entry"); + if (created.length () && created.find ("/") == std::string::npos) + { + 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, entered); + table.addCell (row, 4, started); + table.addCell (row, 5, due); + table.addCell (row, 6, age); + table.addCell (row, 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 << std::endl + << table.render () + << std::endl + << table.rowCount () + << (table.rowCount () == 1 ? " task" : " tasks") + << std::endl; + else + std::cout << "No matches." << std::endl; + +/* + if (count) + std::cout << std::endl + << "[gc: " + << count + << " transferred]" + << 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. + std::map allProjects; + std::vector pending; + tdb.pendingT (pending); + for (unsigned int i = 0; i < pending.size (); ++i) + { + T task (pending[i]); + allProjects[task.getAttribute ("project")] = false; + } + + std::vector completed; + tdb.completedT (completed); + 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"); + ++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%"); + + 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); + + 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 << std::endl + << table.render () + << std::endl + << 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. + std::vector pending; + tdb.allPendingT (pending); + + // Restrict to matching subset. + std::vector matching; + gatherNextTasks (tdb, task, conf, pending, matching); + + // Determine window size, and set table accordingly. + int width = 80; +#ifdef HAVE_LIBNCURSES + if (conf.get ("curses", true)) + { + WINDOW* w = initscr (); + width = w->_maxx + 1; + endwin (); + } +#endif + + /* int gcCount = */ tdb.gc (); + + // Get the pending tasks. + std::vector tasks; + tdb.pendingT (tasks); + + initializeColorRules (conf); + + // 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"); + table.addColumn ("Age"); + table.addColumn ("Description"); + + table.setColumnUnderline (0); + table.setColumnUnderline (1); + table.setColumnUnderline (2); + table.setColumnUnderline (3); + table.setColumnUnderline (4); + table.setColumnUnderline (5); + 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); + table.setColumnWidth (5, Table::minimum); + table.setColumnWidth (6, Table::flexible); + + table.setColumnJustification (0, Table::right); + table.setColumnJustification (3, Table::right); + table.setColumnJustification (5, Table::right); + + table.sortOn (3, Table::ascendingDate); + table.sortOn (2, Table::descendingPriority); + table.sortOn (1, Table::ascendingCharacter); + + // Split any description specified into words. + std::vector descWords; + split (descWords, 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. + foreach (i, matching) + { + T refTask (pending[*i]); + + // Apply description filter. + unsigned int matches = 0; + for (unsigned int w = 0; w < descWords.size (); ++w) + if (refTask.getDescription ().find (descWords[w]) != std::string::npos) + ++matches; + + if (matches == descWords.size ()) + { + // Apply attribute filter. + matches = 0; + foreach (a, attrList) + 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 ()) + { + // Now format the matching task. + bool imminent = false; + bool overdue = false; + Date now; + std::string due = refTask.getAttribute ("due"); + if (due.length () && due.find ("/") == std::string::npos) + { + Date dt (::atoi (due.c_str ())); + int m, d, y; + dt.toMDY (m, d, y); + char formatted[12]; + sprintf (formatted, "%d/%d/%04d", m, d, y); + due = formatted; + + overdue = (dt < now) ? true : false; + now += 7 * 86400; + imminent = dt < now ? true : false; + } + + std::string active; + if (refTask.getAttribute ("start") != "") + active = "*"; + + std::string age; + std::string created = refTask.getAttribute ("entry"); + if (created.length () && created.find ("/") == std::string::npos) + { + 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); + table.addCell (row, 5, age); + table.addCell (row, 6, 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 << std::endl + << table.render () + << std::endl + << table.rowCount () + << (table.rowCount () == 1 ? " task" : " tasks") + << std::endl; + else + std::cout << "No matches." + << std::endl; + +/* + if (gcCount) + std::cout << std::endl + << "[gc: " + << gcCount + << " transferred]" + << 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. + std::vector pending; + tdb.allPendingT (pending); + 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); + 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.addColumn ("Year"); + table.addColumn ("Month"); + table.addColumn ("Added"); + table.addColumn ("Completed"); + table.addColumn ("Deleted"); + table.addColumn ("Net"); + + 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); + + const char *months[] = + { + "January", "February", "March", "April", "May", "June", + "July", "August", "September", "October", "November", "December", + }; + + int priorYear = 0; + foreach (i, groups) + { + int row = table.addRow (); + + 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, months[m - 1]); + + 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 ()) + std::cout << std::endl + << table.render () + << 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); + + 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"); + table.setColumnUnderline (0); + table.setColumnUnderline (1); + table.setColumnJustification (1, Table::right); + table.sortOn (1, Table::descendingNumeric); + + 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 << std::endl + << 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; +} + +//////////////////////////////////////////////////////////////////////////////// +void handleReportCalendar (const TDB& tdb, T& task, Config& conf) +{ + // Today. + Date date; + int m = date.month (); + int y = date.year (); + int today = date.day (); + + // Read all the tasks, filter by those that have a due date. + std::vector annotations; + std::vector pending; + tdb.pendingT (pending); + for (unsigned int i = 0; i < pending.size (); ++i) + { + T task (pending[i]); + if (task.getAttribute ("due") != "") + { + Date d (::atoi (task.getAttribute ("due").c_str ())); + if (d.year () == y && d.month () == m) + annotations.push_back (d.day ()); + } + } + + pending.clear (); + + Table table; + table.addColumn ("Su"); + table.addColumn ("Mo"); + table.addColumn ("Tu"); + table.addColumn ("We"); + table.addColumn ("Th"); + table.addColumn ("Fr"); + table.addColumn ("Sa"); + + table.setColumnUnderline (0); + table.setColumnUnderline (1); + table.setColumnUnderline (2); + table.setColumnUnderline (3); + table.setColumnUnderline (4); + table.setColumnUnderline (5); + table.setColumnUnderline (6); + + table.setColumnJustification (0, Table::right); + table.setColumnJustification (1, Table::right); + table.setColumnJustification (2, Table::right); + table.setColumnJustification (3, Table::right); + table.setColumnJustification (4, Table::right); + table.setColumnJustification (5, Table::right); + table.setColumnJustification (6, Table::right); + + int days = Date::daysInMonth (m, y); + int row = table.addRow (); + for (int d = 1; d <= days; ++d) + { + Date temp (m, d, y); + int dow = temp.dayOfWeek (); + + table.addCell (row, dow, d); + + if (conf.get ("color", true) && d == today) + table.setCellFg (row, dow, Text::cyan); + + for (unsigned int a = 0; a < annotations.size (); ++a) + { + if (conf.get ("color", true) && annotations[a] == d) + { + table.setCellFg (row, dow, Text::black); + table.setCellBg (row, dow, d < today ? Text::red : Text::yellow); + } + } + + if (dow == 6 && d < days) + row = table.addRow (); + } + + std::cout << std::endl + << Date::monthName (m) + << " " + << y + << std::endl + << std::endl + << table.render () + << std::endl; +} + +//////////////////////////////////////////////////////////////////////////////// +void handleReportActive (const TDB& tdb, T& task, Config& conf) +{ + // Determine window size, and set table accordingly. + int width = 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); + + initializeColorRules (conf); + + // Create a table for output. + Table table; + table.setTableWidth (width); + table.addColumn ("ID"); + table.addColumn ("Project"); + table.addColumn ("Pri"); + table.addColumn ("Due"); + table.addColumn ("Description"); + + 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); + + // Split any description specified into words. + std::vector descWords; + split (descWords, 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 < tasks.size (); ++i) + { + T refTask (tasks[i]); + if (refTask.getAttribute ("start") != "") + { + bool imminent = false; + bool overdue = false; + std::string due = refTask.getAttribute ("due"); + if (due.length () && due.find ("/") == std::string::npos) + { + Date dt (::atoi (due.c_str ())); + int m, d, y; + dt.toMDY (m, d, y); + char formatted[12]; + sprintf (formatted, "%d/%d/%04d", m, d, y); + due = formatted; + + Date now; + overdue = dt < now ? true : false; + now += 7 * 86400; + imminent = dt < now ? true : false; + } + + // 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 << std::endl + << table.render () + << std::endl + << 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 = 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); + + initializeColorRules (conf); + + // Create a table for output. + Table table; + table.setTableWidth (width); + table.addColumn ("ID"); + table.addColumn ("Project"); + table.addColumn ("Pri"); + table.addColumn ("Due"); + table.addColumn ("Description"); + + 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); + + // Split any description specified into words. + std::vector descWords; + split (descWords, 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); + + 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 () && due.find ("/") == std::string::npos) + { + Date dt (::atoi (due.c_str ())); + int m, d, y; + dt.toMDY (m, d, y); + char formatted[12]; + sprintf (formatted, "%d/%d/%04d", m, d, y); + due = formatted; + + // 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 << std::endl + << table.render () + << std::endl + << table.rowCount () + << (table.rowCount () == 1 ? " task" : " tasks") + << std::endl; + else + std::cout << "No overdue tasks." << std::endl; +} + +//////////////////////////////////////////////////////////////////////////////// +void handleReportStats (const TDB& tdb, T& task, Config& conf) +{ + // Get all the tasks. + std::vector tasks; + tdb.allT (tasks); + + 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; + 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; + + 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; + } + + // TODO Unused feature list + + std::cout << "Pending " << pendingT << std::endl + << "Completed " << completedT << std::endl + << "Deleted " << deletedT << std::endl + << "Total " << totalT << std::endl; + + Date e (earliest); + std::cout << "Oldest task " << e.toString () << std::endl; + Date l (latest); + std::cout << "Newest task " << l.toString () << 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 = 80; +#ifdef HAVE_LIBNCURSES + if (conf.get ("curses", true)) + { + WINDOW* w = initscr (); + width = w->_maxx + 1; + endwin (); + } +#endif + + // Handle case for zero width on mobile device. + if (width == 0) + width = 80; + + // Create a table for output. + Table table; + table.setTableWidth (width); + table.addColumn ("Config variable"); + table.addColumn ("Value"); + 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); + 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 Paul Beckingham." + << std::endl + << PACKAGE + << " " + << VERSION + << 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?")) + 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) +{ + std::string file = trim (task.getDescription ()); + 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); + 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) +{ + std::cout << std::endl << "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 + + << 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) +{ + std::string nagMessage = conf.get ("nag", std::string ("")); + if (nagMessage != "") + { + // Load all pending. + std::vector pending; + tdb.allPendingT (pending); + + // Restrict to matching subset. + std::vector matching; + gatherNextTasks (tdb, task, conf, pending, matching); + + foreach (i, matching) + if (pending[*i].getId () == task.getId ()) + return; + + std::cout << nagMessage << std::endl; + } +} + +//////////////////////////////////////////////////////////////////////////////// +