diff --git a/ChangeLog b/ChangeLog index 71550c681..32076034e 100644 --- a/ChangeLog +++ b/ChangeLog @@ -6,6 +6,8 @@ implicit "task info 123" command (thanks to John Florian). + Added feature #326, allowing tasks to be added in the completed state, by using the 'log' command in place of 'add' (thanks to Cory Donnelly). + + Added features #36 and #37, providing annual versions of the 'history' + and 'ghistory' command as 'history.annual' and 'ghistory.annual'. ------ old releases ------------------------------ diff --git a/NEWS b/NEWS index ab8acde77..40f6ee282 100644 --- a/NEWS +++ b/NEWS @@ -16,6 +16,7 @@ New Features in task 1.9 - Ability to do case-sensitive or case-insensitive search for keywords, and substitutions in the description and annotations. - New 'log' command to add tasks that are already completed. + - New annual history and ghistory command variations. - Task is now part of Debian Please refer to the ChangeLog file for full details. There are too many to diff --git a/doc/man/task.1 b/doc/man/task.1 index efa90920b..84aa09eef 100644 --- a/doc/man/task.1 +++ b/doc/man/task.1 @@ -106,11 +106,19 @@ Shows a weekly report of tasks completed and started. .TP .B history -Shows a report of task history by month. +Shows a report of task history by month. Alias to history.monthly. + +.TP +.B history.annual +Shows a report of task history by year. .TP .B ghistory -Shows a graphical report of task status by month. +Shows a graphical report of task status by month. Alias to ghistory.monthly. + +.TP +.B ghistory.annual +Shows a graphical report of task status by year. .TP .B calendar [ y | due [y] | month year [y] | year ] diff --git a/i18n/strings.en-US b/i18n/strings.en-US index 597faab08..5c50550d2 100644 --- a/i18n/strings.en-US +++ b/i18n/strings.en-US @@ -42,8 +42,8 @@ 210 edit 211 export 212 help -213 history -214 ghistory +213 history.monthly +214 ghistory.monthly 215 import 216 info 217 prepend @@ -59,6 +59,9 @@ 227 undo 228 version 229 shell +230 config +231 history.annual +232 ghistory.annual # 3xx Attributes - must be sequential 300 project diff --git a/src/Cmd.cpp b/src/Cmd.cpp index 0e049f4f2..4798f1676 100644 --- a/src/Cmd.cpp +++ b/src/Cmd.cpp @@ -111,36 +111,38 @@ void Cmd::load () commands.push_back ("_ids"); commands.push_back ("_config"); commands.push_back ("_version"); - commands.push_back (context.stringtable.get (CMD_ADD, "add")); - commands.push_back (context.stringtable.get (CMD_APPEND, "append")); - commands.push_back (context.stringtable.get (CMD_ANNOTATE, "annotate")); - commands.push_back (context.stringtable.get (CMD_CALENDAR, "calendar")); - commands.push_back (context.stringtable.get (CMD_COLORS, "colors")); - commands.push_back (context.stringtable.get (CMD_CONFIG, "config")); - commands.push_back (context.stringtable.get (CMD_DELETE, "delete")); - commands.push_back (context.stringtable.get (CMD_DONE, "done")); - commands.push_back (context.stringtable.get (CMD_DUPLICATE, "duplicate")); - commands.push_back (context.stringtable.get (CMD_EDIT, "edit")); - commands.push_back (context.stringtable.get (CMD_EXPORT, "export")); - commands.push_back (context.stringtable.get (CMD_HELP, "help")); - commands.push_back (context.stringtable.get (CMD_HISTORY, "history")); - commands.push_back (context.stringtable.get (CMD_GHISTORY, "ghistory")); - commands.push_back (context.stringtable.get (CMD_IMPORT, "import")); - commands.push_back (context.stringtable.get (CMD_INFO, "info")); - commands.push_back (context.stringtable.get (CMD_LOG, "log")); - commands.push_back (context.stringtable.get (CMD_PREPEND, "prepend")); - commands.push_back (context.stringtable.get (CMD_PROJECTS, "projects")); + commands.push_back (context.stringtable.get (CMD_ADD, "add")); + commands.push_back (context.stringtable.get (CMD_APPEND, "append")); + commands.push_back (context.stringtable.get (CMD_ANNOTATE, "annotate")); + commands.push_back (context.stringtable.get (CMD_CALENDAR, "calendar")); + commands.push_back (context.stringtable.get (CMD_COLORS, "colors")); + commands.push_back (context.stringtable.get (CMD_CONFIG, "config")); + commands.push_back (context.stringtable.get (CMD_DELETE, "delete")); + commands.push_back (context.stringtable.get (CMD_DONE, "done")); + commands.push_back (context.stringtable.get (CMD_DUPLICATE, "duplicate")); + commands.push_back (context.stringtable.get (CMD_EDIT, "edit")); + commands.push_back (context.stringtable.get (CMD_EXPORT, "export")); + commands.push_back (context.stringtable.get (CMD_HELP, "help")); + commands.push_back (context.stringtable.get (CMD_HISTORY_MONTHLY, "history.monthly")); + commands.push_back (context.stringtable.get (CMD_HISTORY_ANNUAL, "history.annual")); + commands.push_back (context.stringtable.get (CMD_GHISTORY_MONTHLY, "ghistory.monthly")); + commands.push_back (context.stringtable.get (CMD_GHISTORY_ANNUAL, "ghistory.annual")); + commands.push_back (context.stringtable.get (CMD_IMPORT, "import")); + commands.push_back (context.stringtable.get (CMD_INFO, "info")); + commands.push_back (context.stringtable.get (CMD_LOG, "log")); + commands.push_back (context.stringtable.get (CMD_PREPEND, "prepend")); + commands.push_back (context.stringtable.get (CMD_PROJECTS, "projects")); #ifdef FEATURE_SHELL - commands.push_back (context.stringtable.get (CMD_SHELL, "shell")); + commands.push_back (context.stringtable.get (CMD_SHELL, "shell")); #endif - commands.push_back (context.stringtable.get (CMD_START, "start")); - commands.push_back (context.stringtable.get (CMD_STATS, "stats")); - commands.push_back (context.stringtable.get (CMD_STOP, "stop")); - commands.push_back (context.stringtable.get (CMD_SUMMARY, "summary")); - commands.push_back (context.stringtable.get (CMD_TAGS, "tags")); - commands.push_back (context.stringtable.get (CMD_TIMESHEET, "timesheet")); - commands.push_back (context.stringtable.get (CMD_UNDO, "undo")); - commands.push_back (context.stringtable.get (CMD_VERSION, "version")); + commands.push_back (context.stringtable.get (CMD_START, "start")); + commands.push_back (context.stringtable.get (CMD_STATS, "stats")); + commands.push_back (context.stringtable.get (CMD_STOP, "stop")); + commands.push_back (context.stringtable.get (CMD_SUMMARY, "summary")); + commands.push_back (context.stringtable.get (CMD_TAGS, "tags")); + commands.push_back (context.stringtable.get (CMD_TIMESHEET, "timesheet")); + commands.push_back (context.stringtable.get (CMD_UNDO, "undo")); + commands.push_back (context.stringtable.get (CMD_VERSION, "version")); // Now load the custom reports. std::vector all; @@ -190,27 +192,29 @@ void Cmd::allCommands (std::vector & all) const // Commands that do not directly modify the data files. bool Cmd::isReadOnlyCommand () { - if (command == "_projects" || - command == "_tags" || - command == "_commands" || - command == "_ids" || - command == "_config" || - command == "_version" || - command == context.stringtable.get (CMD_CALENDAR, "calendar") || - command == context.stringtable.get (CMD_COLORS, "colors") || - command == context.stringtable.get (CMD_CONFIG, "config") || - command == context.stringtable.get (CMD_EXPORT, "export") || - command == context.stringtable.get (CMD_HELP, "help") || - command == context.stringtable.get (CMD_HISTORY, "history") || - command == context.stringtable.get (CMD_GHISTORY, "ghistory") || - command == context.stringtable.get (CMD_INFO, "info") || - command == context.stringtable.get (CMD_PROJECTS, "projects") || - command == context.stringtable.get (CMD_SHELL, "shell") || - command == context.stringtable.get (CMD_STATS, "stats") || - command == context.stringtable.get (CMD_SUMMARY, "summary") || - command == context.stringtable.get (CMD_TAGS, "tags") || - command == context.stringtable.get (CMD_TIMESHEET, "timesheet") || - command == context.stringtable.get (CMD_VERSION, "version") || + if (command == "_projects" || + command == "_tags" || + command == "_commands" || + command == "_ids" || + command == "_config" || + command == "_version" || + command == context.stringtable.get (CMD_CALENDAR, "calendar") || + command == context.stringtable.get (CMD_COLORS, "colors") || + command == context.stringtable.get (CMD_CONFIG, "config") || + command == context.stringtable.get (CMD_EXPORT, "export") || + command == context.stringtable.get (CMD_HELP, "help") || + command == context.stringtable.get (CMD_HISTORY_MONTHLY, "history.monthly") || + command == context.stringtable.get (CMD_HISTORY_ANNUAL, "history.annual") || + command == context.stringtable.get (CMD_GHISTORY_MONTHLY, "ghistory.monthly") || + command == context.stringtable.get (CMD_GHISTORY_ANNUAL, "ghistory.annual") || + command == context.stringtable.get (CMD_INFO, "info") || + command == context.stringtable.get (CMD_PROJECTS, "projects") || + command == context.stringtable.get (CMD_SHELL, "shell") || + command == context.stringtable.get (CMD_STATS, "stats") || + command == context.stringtable.get (CMD_SUMMARY, "summary") || + command == context.stringtable.get (CMD_TAGS, "tags") || + command == context.stringtable.get (CMD_TIMESHEET, "timesheet") || + command == context.stringtable.get (CMD_VERSION, "version") || validCustom (command)) return true; diff --git a/src/Config.cpp b/src/Config.cpp index 837e74e19..75e23460a 100644 --- a/src/Config.cpp +++ b/src/Config.cpp @@ -161,6 +161,8 @@ std::string Config::defaults = "\n" "# Aliases - alternate names for commands\n" "alias.rm=delete # Alias for the delete command\n" + "alias.history=history.monthly # Prefer monthly history reports\n" + "alias.ghistory=ghistory.monthly # Prefer monthly graphical history reports\n" "\n" "# Fields: id,uuid,project,priority,priority_long,entry,entry_time,\n" // TODO "# start,entry_time,due,recur,recurrence_indicator,age,\n" // TODO diff --git a/src/Context.cpp b/src/Context.cpp index 54da86a74..bee94fa5d 100644 --- a/src/Context.cpp +++ b/src/Context.cpp @@ -205,53 +205,55 @@ int Context::dispatch (std::string &out) hooks.trigger ("pre-dispatch"); // TODO Just look at this thing. It cries out for a dispatch table. - if (cmd.command == "projects") { rc = handleProjects (out); } - else if (cmd.command == "tags") { rc = handleTags (out); } - else if (cmd.command == "colors") { rc = handleColor (out); } - else if (cmd.command == "version") { rc = handleVersion (out); } - else if (cmd.command == "config") { rc = handleConfig (out); } - else if (cmd.command == "help") { rc = longUsage (out); } - else if (cmd.command == "stats") { rc = handleReportStats (out); } - else if (cmd.command == "info") { rc = handleInfo (out); } - else if (cmd.command == "history") { rc = handleReportHistory (out); } - else if (cmd.command == "ghistory") { rc = handleReportGHistory (out); } - else if (cmd.command == "summary") { rc = handleReportSummary (out); } - else if (cmd.command == "calendar") { rc = handleReportCalendar (out); } - else if (cmd.command == "timesheet") { rc = handleReportTimesheet (out); } - else if (cmd.command == "add") { rc = handleAdd (out); } - else if (cmd.command == "log") { rc = handleLog (out); } - else if (cmd.command == "append") { rc = handleAppend (out); } - else if (cmd.command == "prepend") { rc = handlePrepend (out); } - else if (cmd.command == "annotate") { rc = handleAnnotate (out); } - else if (cmd.command == "done") { rc = handleDone (out); } - else if (cmd.command == "delete") { rc = handleDelete (out); } - else if (cmd.command == "start") { rc = handleStart (out); } - else if (cmd.command == "stop") { rc = handleStop (out); } - else if (cmd.command == "export") { rc = handleExport (out); } - else if (cmd.command == "import") { rc = handleImport (out); } - else if (cmd.command == "duplicate") { rc = handleDuplicate (out); } - else if (cmd.command == "edit") { rc = handleEdit (out); } + if (cmd.command == "projects") { rc = handleProjects (out); } + else if (cmd.command == "tags") { rc = handleTags (out); } + else if (cmd.command == "colors") { rc = handleColor (out); } + else if (cmd.command == "version") { rc = handleVersion (out); } + else if (cmd.command == "config") { rc = handleConfig (out); } + else if (cmd.command == "help") { rc = longUsage (out); } + else if (cmd.command == "stats") { rc = handleReportStats (out); } + else if (cmd.command == "info") { rc = handleInfo (out); } + else if (cmd.command == "history.monthly") { rc = handleReportHistoryMonthly (out); } + else if (cmd.command == "history.annual") { rc = handleReportHistoryAnnual (out); } + else if (cmd.command == "ghistory.monthly") { rc = handleReportGHistoryMonthly (out); } + else if (cmd.command == "ghistory.annual") { rc = handleReportGHistoryAnnual (out); } + else if (cmd.command == "summary") { rc = handleReportSummary (out); } + else if (cmd.command == "calendar") { rc = handleReportCalendar (out); } + else if (cmd.command == "timesheet") { rc = handleReportTimesheet (out); } + else if (cmd.command == "add") { rc = handleAdd (out); } + else if (cmd.command == "log") { rc = handleLog (out); } + else if (cmd.command == "append") { rc = handleAppend (out); } + else if (cmd.command == "prepend") { rc = handlePrepend (out); } + else if (cmd.command == "annotate") { rc = handleAnnotate (out); } + else if (cmd.command == "done") { rc = handleDone (out); } + else if (cmd.command == "delete") { rc = handleDelete (out); } + else if (cmd.command == "start") { rc = handleStart (out); } + else if (cmd.command == "stop") { rc = handleStop (out); } + else if (cmd.command == "export") { rc = handleExport (out); } + else if (cmd.command == "import") { rc = handleImport (out); } + else if (cmd.command == "duplicate") { rc = handleDuplicate (out); } + else if (cmd.command == "edit") { rc = handleEdit (out); } #ifdef FEATURE_SHELL - else if (cmd.command == "shell") { handleShell ( ); } + else if (cmd.command == "shell") { handleShell ( ); } #endif - else if (cmd.command == "undo") { handleUndo ( ); } - else if (cmd.command == "_projects") { rc = handleCompletionProjects (out); } - else if (cmd.command == "_tags") { rc = handleCompletionTags (out); } - else if (cmd.command == "_commands") { rc = handleCompletionCommands (out); } - else if (cmd.command == "_ids") { rc = handleCompletionIDs (out); } - else if (cmd.command == "_config") { rc = handleCompletionConfig (out); } - else if (cmd.command == "_version") { rc = handleCompletionVersion (out); } + else if (cmd.command == "undo") { handleUndo ( ); } + else if (cmd.command == "_projects") { rc = handleCompletionProjects (out); } + else if (cmd.command == "_tags") { rc = handleCompletionTags (out); } + else if (cmd.command == "_commands") { rc = handleCompletionCommands (out); } + else if (cmd.command == "_ids") { rc = handleCompletionIDs (out); } + else if (cmd.command == "_config") { rc = handleCompletionConfig (out); } + else if (cmd.command == "_version") { rc = handleCompletionVersion (out); } else if (cmd.command == "" && - sequence.size ()) { rc = handleModify (out); } + sequence.size ()) { rc = handleModify (out); } // Command that display IDs and therefore need TDB::gc first. - else if (cmd.command == "next") { if (!inShadow) tdb.gc (); rc = handleReportNext (out); } - else if (cmd.validCustom (cmd.command)) { if (!inShadow) tdb.gc (); rc = handleCustomReport (cmd.command, out); } + else if (cmd.command == "next") { if (!inShadow) tdb.gc (); rc = handleReportNext (out); } + else if (cmd.validCustom (cmd.command)) { if (!inShadow) tdb.gc (); rc = handleCustomReport (cmd.command, out); } // If the command is not recognized, display usage. - else { hooks.trigger ("pre-usage-command"); - rc = shortUsage (out); - hooks.trigger ("post-usage-command"); } + else { hooks.trigger ("pre-usage-command"); + rc = shortUsage (out); + hooks.trigger ("post-usage-command"); } // Only update the shadow file if such an update was not suppressed (shadow), if (cmd.isWriteCommand () && !inShadow) diff --git a/src/i18n.h b/src/i18n.h index 22aea4737..8a4e00984 100644 --- a/src/i18n.h +++ b/src/i18n.h @@ -79,8 +79,8 @@ #define CMD_EDIT 210 #define CMD_EXPORT 211 #define CMD_HELP 212 -#define CMD_HISTORY 213 -#define CMD_GHISTORY 214 +#define CMD_HISTORY_MONTHLY 213 +#define CMD_GHISTORY_MONTHLY 214 #define CMD_IMPORT 215 #define CMD_INFO 216 #define CMD_PREPEND 217 @@ -97,6 +97,8 @@ #define CMD_VERSION 228 #define CMD_SHELL 229 #define CMD_CONFIG 230 +#define CMD_HISTORY_ANNUAL 231 +#define CMD_GHISTORY_ANNUAL 232 // 3xx Attributes #define ATT_PROJECT 300 diff --git a/src/main.h b/src/main.h index 9666de9a3..60c1839c0 100644 --- a/src/main.h +++ b/src/main.h @@ -98,8 +98,10 @@ int longUsage (std::string &); int handleInfo (std::string &); int handleReportSummary (std::string &); int handleReportNext (std::string &); -int handleReportHistory (std::string &); -int handleReportGHistory (std::string &); +int handleReportHistoryMonthly (std::string &); +int handleReportHistoryAnnual (std::string &); +int handleReportGHistoryMonthly (std::string &); +int handleReportGHistoryAnnual (std::string &); int handleReportCalendar (std::string &); int handleReportStats (std::string &); int handleReportTimesheet (std::string &); diff --git a/src/report.cpp b/src/report.cpp index 3db83f3b8..52d507278 100644 --- a/src/report.cpp +++ b/src/report.cpp @@ -164,11 +164,19 @@ int shortUsage (std::string &outs) row = table.addRow (); table.addCell (row, 1, "task history"); - table.addCell (row, 2, "Shows a report of task history, by month."); + table.addCell (row, 2, "Shows a report of task history, by month. Alias to history.monthly."); + + row = table.addRow (); + table.addCell (row, 1, "task history.annual"); + table.addCell (row, 2, "Shows a report of task history, by year."); row = table.addRow (); table.addCell (row, 1, "task ghistory"); - table.addCell (row, 2, "Shows a graphical report of task history, by month."); + table.addCell (row, 2, "Shows a graphical report of task history, by month. Alias to ghistory.monthly."); + + row = table.addRow (); + table.addCell (row, 1, "task ghistory.annual"); + table.addCell (row, 2, "Shows a graphical report of task history, by year."); row = table.addRow (); table.addCell (row, 1, "task calendar [due|month year|year]"); @@ -827,7 +835,25 @@ time_t monthlyEpoch (const std::string& date) return 0; } -int handleReportHistory (std::string &outs) +time_t yearlyEpoch (const std::string& date) +{ + // Convert any date in epoch form to m/d/y, then convert back + // to epoch form for the date 1/1/y. + if (date.length ()) + { + Date d1 (atoi (date.c_str ())); + int m, d, y; + d1.toMDY (m, d, y); + Date d2 (1, 1, y); + time_t epoch; + d2.toEpoch (epoch); + return epoch; + } + + return 0; +} + +int handleReportHistoryMonthly (std::string &outs) { int rc = 0; @@ -993,8 +1019,171 @@ int handleReportHistory (std::string &outs) return rc; } +int handleReportHistoryAnnual (std::string &outs) +{ + int rc = 0; + + if (context.hooks.trigger ("pre-history-command")) + { + std::map groups; // Represents any month with data + std::map addedGroup; // Additions by month + std::map completedGroup; // Completions by month + std::map deletedGroup; // Deletions by month + + // Scan the pending tasks. + std::vector tasks; + context.tdb.lock (context.config.getBoolean ("locking")); + handleRecurrence (); + context.tdb.load (tasks, context.filter); + context.tdb.commit (); + context.tdb.unlock (); + + foreach (task, tasks) + { + time_t epoch = yearlyEpoch (task->get ("entry")); + groups[epoch] = 0; + + // Every task has an entry date. + if (addedGroup.find (epoch) != addedGroup.end ()) + addedGroup[epoch] = addedGroup[epoch] + 1; + else + addedGroup[epoch] = 1; + + // All deleted tasks have an end date. + if (task->getStatus () == Task::deleted) + { + epoch = yearlyEpoch (task->get ("end")); + groups[epoch] = 0; + + if (deletedGroup.find (epoch) != deletedGroup.end ()) + deletedGroup[epoch] = deletedGroup[epoch] + 1; + else + deletedGroup[epoch] = 1; + } + + // All completed tasks have an end date. + else if (task->getStatus () == Task::completed) + { + epoch = yearlyEpoch (task->get ("end")); + groups[epoch] = 0; + + if (completedGroup.find (epoch) != completedGroup.end ()) + completedGroup[epoch] = completedGroup[epoch] + 1; + else + completedGroup[epoch] = 1; + } + } + + // Now build the table. + Table table; + table.setDateFormat (context.config.get ("dateformat")); + table.addColumn ("Year"); + table.addColumn ("Added"); + table.addColumn ("Completed"); + table.addColumn ("Deleted"); + table.addColumn ("Net"); + + if ((context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor")) && + context.config.getBoolean ("fontunderline")) + { + table.setColumnUnderline (0); + table.setColumnUnderline (1); + table.setColumnUnderline (2); + table.setColumnUnderline (3); + table.setColumnUnderline (4); + } + else + table.setTableDashedUnderline (); + + table.setColumnJustification (1, Table::right); + table.setColumnJustification (2, Table::right); + table.setColumnJustification (3, Table::right); + table.setColumnJustification (4, 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; + } + + int net = 0; + + if (addedGroup.find (i->first) != addedGroup.end ()) + { + table.addCell (row, 1, addedGroup[i->first]); + net +=addedGroup[i->first]; + } + + if (completedGroup.find (i->first) != completedGroup.end ()) + { + table.addCell (row, 2, completedGroup[i->first]); + net -= completedGroup[i->first]; + } + + if (deletedGroup.find (i->first) != deletedGroup.end ()) + { + table.addCell (row, 3, deletedGroup[i->first]); + net -= deletedGroup[i->first]; + } + + table.addCell (row, 4, net); + if ((context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor")) && net) + table.setCellColor (row, 4, net > 0 ? Color (Color::red) : + Color (Color::green)); + } + + if (table.rowCount ()) + { + table.addRow (); + row = table.addRow (); + + table.addCell (row, 0, "Average"); + if (context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor")) + table.setRowColor (row, Color (Color::nocolor, Color::nocolor, false, true, false)); + table.addCell (row, 1, totalAdded / (table.rowCount () - 2)); + table.addCell (row, 2, totalCompleted / (table.rowCount () - 2)); + table.addCell (row, 3, totalDeleted / (table.rowCount () - 2)); + table.addCell (row, 4, (totalAdded - totalCompleted - totalDeleted) / (table.rowCount () - 2)); + } + + std::stringstream out; + if (table.rowCount ()) + out << optionalBlankLine () + << table.render () + << std::endl; + else + { + out << "No tasks." << std::endl; + rc = 1; + } + + outs = out.str (); + context.hooks.trigger ("post-history-command"); + } + + return rc; +} + //////////////////////////////////////////////////////////////////////////////// -int handleReportGHistory (std::string &outs) +int handleReportGHistoryMonthly (std::string &outs) { int rc = 0; @@ -1203,6 +1392,213 @@ int handleReportGHistory (std::string &outs) return rc; } +//////////////////////////////////////////////////////////////////////////////// +int handleReportGHistoryAnnual (std::string &outs) +{ + int rc = 0; + + if (context.hooks.trigger ("pre-ghistory-command")) + { + std::map groups; // Represents any month with data + std::map addedGroup; // Additions by month + std::map completedGroup; // Completions by month + std::map deletedGroup; // Deletions by month + + // Scan the pending tasks. + std::vector tasks; + context.tdb.lock (context.config.getBoolean ("locking")); + handleRecurrence (); + context.tdb.load (tasks, context.filter); + context.tdb.commit (); + context.tdb.unlock (); + + foreach (task, tasks) + { + time_t epoch = yearlyEpoch (task->get ("entry")); + groups[epoch] = 0; + + // Every task has an entry date. + if (addedGroup.find (epoch) != addedGroup.end ()) + addedGroup[epoch] = addedGroup[epoch] + 1; + else + addedGroup[epoch] = 1; + + // All deleted tasks have an end date. + if (task->getStatus () == Task::deleted) + { + epoch = yearlyEpoch (task->get ("end")); + groups[epoch] = 0; + + if (deletedGroup.find (epoch) != deletedGroup.end ()) + deletedGroup[epoch] = deletedGroup[epoch] + 1; + else + deletedGroup[epoch] = 1; + } + + // All completed tasks have an end date. + else if (task->getStatus () == Task::completed) + { + epoch = yearlyEpoch (task->get ("end")); + groups[epoch] = 0; + + if (completedGroup.find (epoch) != completedGroup.end ()) + completedGroup[epoch] = completedGroup[epoch] + 1; + else + completedGroup[epoch] = 1; + } + } + + int widthOfBar = context.getWidth () - 15; // 15 == strlen ("2008 September ") + + // Now build the table. + Table table; + table.setDateFormat (context.config.get ("dateformat")); + table.addColumn ("Year"); + table.addColumn ("Number Added/Completed/Deleted"); + + if ((context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor")) && + context.config.getBoolean ("fontunderline")) + { + table.setColumnUnderline (0); + } + else + table.setTableDashedUnderline (); + + Color color_added (Color::black, Color::red); + Color color_completed (Color::black, Color::green); + Color color_deleted (Color::black, Color::yellow); + + // Determine the longest line, and the longest "added" line. + int maxAddedLine = 0; + int maxRemovedLine = 0; + foreach (i, groups) + { + if (completedGroup[i->first] + deletedGroup[i->first] > maxRemovedLine) + maxRemovedLine = completedGroup[i->first] + deletedGroup[i->first]; + + if (addedGroup[i->first] > maxAddedLine) + maxAddedLine = addedGroup[i->first]; + } + + int maxLine = maxAddedLine + maxRemovedLine; + if (maxLine > 0) + { + unsigned int leftOffset = (widthOfBar * maxAddedLine) / maxLine; + + int totalAdded = 0; + int totalCompleted = 0; + int totalDeleted = 0; + + int priorYear = 0; + int row = 0; + foreach (i, groups) + { + row = table.addRow (); + + totalAdded += addedGroup[i->first]; + totalCompleted += completedGroup[i->first]; + totalDeleted += deletedGroup[i->first]; + + Date dt (i->first); + int m, d, y; + dt.toMDY (m, d, y); + + if (y != priorYear) + { + table.addCell (row, 0, y); + priorYear = y; + } + + unsigned int addedBar = (widthOfBar * addedGroup[i->first]) / maxLine; + unsigned int completedBar = (widthOfBar * completedGroup[i->first]) / maxLine; + unsigned int deletedBar = (widthOfBar * deletedGroup[i->first]) / maxLine; + + std::string bar = ""; + if (context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor")) + { + char number[24]; + std::string aBar = ""; + if (addedGroup[i->first]) + { + sprintf (number, "%d", addedGroup[i->first]); + aBar = number; + while (aBar.length () < addedBar) + aBar = " " + aBar; + } + + std::string cBar = ""; + if (completedGroup[i->first]) + { + sprintf (number, "%d", completedGroup[i->first]); + cBar = number; + while (cBar.length () < completedBar) + cBar = " " + cBar; + } + + std::string dBar = ""; + if (deletedGroup[i->first]) + { + sprintf (number, "%d", deletedGroup[i->first]); + dBar = number; + while (dBar.length () < deletedBar) + dBar = " " + dBar; + } + + while (bar.length () < leftOffset - aBar.length ()) + bar += " "; + + bar += color_added.colorize (aBar); + bar += color_completed.colorize (cBar); + bar += color_deleted.colorize (dBar); + } + else + { + std::string aBar = ""; while (aBar.length () < addedBar) aBar += "+"; + std::string cBar = ""; while (cBar.length () < completedBar) cBar += "X"; + std::string dBar = ""; while (dBar.length () < deletedBar) dBar += "-"; + + while (bar.length () < leftOffset - aBar.length ()) + bar += " "; + + bar += aBar + cBar + dBar; + } + + table.addCell (row, 1, bar); + } + } + + std::stringstream out; + if (table.rowCount ()) + { + out << optionalBlankLine () + << table.render () + << std::endl; + + if (context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor")) + out << "Legend: " + << color_added.colorize ("added") + << ", " + << color_completed.colorize ("completed") + << ", " + << color_deleted.colorize ("deleted") + << optionalBlankLine () + << std::endl; + else + out << "Legend: + added, X completed, - deleted" << std::endl; + } + else + { + out << "No tasks." << std::endl; + rc = 1; + } + + outs = out.str (); + context.hooks.trigger ("post-ghistory-command"); + } + + return rc; +} + //////////////////////////////////////////////////////////////////////////////// int handleReportTimesheet (std::string &outs) { diff --git a/src/tests/cmd.t.cpp b/src/tests/cmd.t.cpp index 971e6a7c2..b1bc60e87 100644 --- a/src/tests/cmd.t.cpp +++ b/src/tests/cmd.t.cpp @@ -33,7 +33,7 @@ Context context; //////////////////////////////////////////////////////////////////////////////// int main (int argc, char** argv) { - UnitTest t (78); + UnitTest t (84); // Without Context::initialize, there is no set of defaults loaded into // Context::Config. @@ -65,13 +65,21 @@ int main (int argc, char** argv) t.ok (cmd.isReadOnlyCommand (), "isReadOnlyCommand help"); t.notok (cmd.isWriteCommand (), "not isWriteCommand help"); - cmd.command = "history"; - t.ok (cmd.isReadOnlyCommand (), "isReadOnlyCommand history"); - t.notok (cmd.isWriteCommand (), "not isWriteCommand history"); + cmd.command = "history.monthly"; + t.ok (cmd.isReadOnlyCommand (), "isReadOnlyCommand history.monthly"); + t.notok (cmd.isWriteCommand (), "not isWriteCommand history.monthly"); - cmd.command = "ghistory"; - t.ok (cmd.isReadOnlyCommand (), "isReadOnlyCommand ghistory"); - t.notok (cmd.isWriteCommand (), "not isWriteCommand ghistory"); + cmd.command = "history.annual"; + t.ok (cmd.isReadOnlyCommand (), "isReadOnlyCommand history.annual"); + t.notok (cmd.isWriteCommand (), "not isWriteCommand history.annual"); + + cmd.command = "ghistory.monthly"; + t.ok (cmd.isReadOnlyCommand (), "isReadOnlyCommand ghistory.monthly"); + t.notok (cmd.isWriteCommand (), "not isWriteCommand ghistory.monthly"); + + cmd.command = "ghistory.annual"; + t.ok (cmd.isReadOnlyCommand (), "isReadOnlyCommand ghistory.annual"); + t.notok (cmd.isWriteCommand (), "not isWriteCommand ghistory.annual"); cmd.command = "info"; t.ok (cmd.isReadOnlyCommand (), "isReadOnlyCommand info"); @@ -121,6 +129,10 @@ int main (int argc, char** argv) t.notok (cmd.isReadOnlyCommand (), "not isReadOnlyCommand add"); t.ok (cmd.isWriteCommand (), "isWriteCommand add"); + cmd.command = "log"; + t.notok (cmd.isReadOnlyCommand (), "not isReadOnlyCommand log"); + t.ok (cmd.isWriteCommand (), "isWriteCommand log"); + cmd.command = "append"; t.notok (cmd.isReadOnlyCommand (), "not isReadOnlyCommand append"); t.ok (cmd.isWriteCommand (), "isWriteCommand append");