diff --git a/ChangeLog b/ChangeLog index 3133f5204..493b2f4d0 100644 --- a/ChangeLog +++ b/ChangeLog @@ -12,6 +12,7 @@ + Corrected sorting to use std::stable_sort instead of std::sort, which is not guaranteed stable (thanks to Stefan Hacker). + Enhanced diagnostics command. + + Performance enhancements. + The old 'curses' configuration variable is now replaced by 'detection', and has the same meaning - whether or not to auto-detect terminal size. + Added Czech Republic holiday files (thanks to Tomas Cech). @@ -26,6 +27,7 @@ + New 'dependency.indicator' configuration variable for the depends.indicator report field. + New 'indent.annotation' for the 'description.default' field format. + + New 'color.label' for colorizing the report column labels. # Tracked Features, sorted by ID. + Added feature #330, which supports the 'inverse' color attribute. diff --git a/NEWS b/NEWS index 64a4ca10f..0bc8672d6 100644 --- a/NEWS +++ b/NEWS @@ -11,6 +11,9 @@ New Features in taskwarrior 2.0.0 - Project names may now contain spaces. - New export-html.pl script. - Now supports the 'inverse' color attribute. + - Reports may now be sorted by columns that are not displayed (example: ID, + project, due date and description sorted by urgency). + - Performance enhancements. Please refer to the ChangeLog file for full details. There are too many to list here. @@ -31,6 +34,7 @@ New configuration options in taskwarrior 2.0.0 - Two new solarized color themes. - New 'dependency.indicator' for the 'depends.indicator' report field format. - New 'indent.annotation' for the description.default field format. + - New 'color.label' for report column labels. Newly deprecated features in taskwarrior 2.0.0 diff --git a/doc/man/taskrc.5.in b/doc/man/taskrc.5.in index 39a1eb41c..a44ae1aa3 100644 --- a/doc/man/taskrc.5.in +++ b/doc/man/taskrc.5.in @@ -212,9 +212,21 @@ and a "+" sign will be added if there are any annotations present. The default value is "full". .TP -.B indent.annotation=1 +.B indent.annotation=2 Controls the number of spaces to indent annotations when shown beneath the -description field. The default value is "1". +description field. The default value is "2". + +.TP +.B indent.report=0 +Controls the indentation of the entire report output. Default is "0". + +.TP +.B row.padding=0 +Controls left and right padding around each row of the report output. Default is "0". + +.TP +.B column.padding=0 +Controls padding between columns of the report output. Default is "1". .TP .B next=2 @@ -744,6 +756,11 @@ Color of holidays in calendar. Color of weeknumbers in calendar. .RE +.TP +.B color.label= +Colors the report labels. Defaults to not use color. +.RE + .TP .B color.alternate=on rgb253 Color of alternate tasks. diff --git a/src/Config.cpp b/src/Config.cpp index 8c358e10b..96dfa352c 100644 --- a/src/Config.cpp +++ b/src/Config.cpp @@ -73,7 +73,10 @@ std::string Config::defaults = "confirmation=yes # Confirmation on delete, big changes\n" "echo.command=yes # Details on command just run\n" "annotations=full # Level of verbosity for annotations: full, sparse or none\n" - "indent.annotation=1 # Indent spaces for annotations\n" + "indent.annotation=2 # Indent spaces for annotations\n" + "indent.report=0 # Indent spaces for whole report\n" + "row.padding=0 # Left and right padding for each row of report\n" + "column.padding=1 # Spaces between each column in a report\n" "next=2 # How many tasks per project in next report\n" "bulk=2 # > 2 tasks considered 'a lot', for confirmation\n" "nag=You have more urgent tasks. # Nag message to keep you honest\n" // TODO diff --git a/src/Task.cpp b/src/Task.cpp index d8201b415..4152e79be 100644 --- a/src/Task.cpp +++ b/src/Task.cpp @@ -42,6 +42,8 @@ extern Context context; //////////////////////////////////////////////////////////////////////////////// Task::Task () : id (0) +, urgency_value (0.0) +, recalc_urgency (true) { } @@ -121,6 +123,8 @@ void Task::setEntry () char entryTime[16]; sprintf (entryTime, "%u", (unsigned int) time (NULL)); set ("entry", entryTime); // No i18n + + recalc_urgency = true; } //////////////////////////////////////////////////////////////////////////////// @@ -133,6 +137,8 @@ Task::status Task::getStatus () const void Task::setStatus (Task::status status) { set ("status", statusToText (status)); // No i18n + + recalc_urgency = true; } //////////////////////////////////////////////////////////////////////////////// @@ -551,6 +557,8 @@ void Task::setAnnotations (const std::vector & annotations) std::vector ::const_iterator ci; for (ci = annotations.begin (); ci != annotations.end (); ++ci) (*this)[ci->name ()] = *ci; + + recalc_urgency = true; } //////////////////////////////////////////////////////////////////////////////// @@ -885,13 +893,14 @@ int Task::determineVersion (const std::string& line) // // See rfc31-urgency.txt for full details. // -double Task::urgency () +float Task::urgency () { - double urgency = 0.0; + if (! recalc_urgency) + return urgency_value; // urgency.priority.coefficient - double coefficient = context.config.getReal ("urgency.priority.coefficient"); - double term; + float coefficient = context.config.getReal ("urgency.priority.coefficient"); + float term; std::string value = get ("priority"); if (value == "H") term = 1.0; @@ -899,7 +908,7 @@ double Task::urgency () else if (value == "L") term = 0.3; else term = 0.0; - urgency += term * coefficient; + urgency_value += term * coefficient; // urgency.project.coefficient coefficient = context.config.getReal ("urgency.project.coefficient"); @@ -908,7 +917,7 @@ double Task::urgency () if (value != "") term = 1.0; else term = 0.0; - urgency += term * coefficient; + urgency_value += term * coefficient; // urgency.active.coefficient coefficient = context.config.getReal ("urgency.active.coefficient"); @@ -917,7 +926,7 @@ double Task::urgency () if (value != "") term = 1.0; else term = 0.0; - urgency += term * coefficient; + urgency_value += term * coefficient; // urgency.waiting.coefficient coefficient = context.config.getReal ("urgency.waiting.coefficient"); @@ -926,7 +935,7 @@ double Task::urgency () if (value == "pending") term = 1.0; else if (value == "waiting") term = 0.0; - urgency += term * coefficient; + urgency_value += term * coefficient; // urgency.blocked.coefficient coefficient = context.config.getReal ("urgency.blocked.coefficient"); @@ -935,7 +944,7 @@ double Task::urgency () if (value != "") term = 1.0; else term = 0.0; - urgency += term * coefficient; + urgency_value += term * coefficient; // urgency.annotations.coefficient coefficient = context.config.getReal ("urgency.annotations.coefficient"); @@ -947,7 +956,7 @@ double Task::urgency () else if (annos.size () == 1) term = 0.8; else term = 0.0; - urgency += term * coefficient; + urgency_value += term * coefficient; // urgency.tags.coefficient coefficient = context.config.getReal ("urgency.tags.coefficient"); @@ -958,7 +967,7 @@ double Task::urgency () else if (count == 1) term = 0.8; else term = 0.0; - urgency += term * coefficient; + urgency_value += term * coefficient; // urgency.next.coefficient coefficient = context.config.getReal ("urgency.next.coefficient"); @@ -966,7 +975,7 @@ double Task::urgency () if (hasTag ("next")) term = 1.0; else term = 0.0; - urgency += term * coefficient; + urgency_value += term * coefficient; // urgency.due.coefficient // overdue days 7 -> 1.0 @@ -1025,7 +1034,7 @@ double Task::urgency () else term = 0.0; - urgency += term * coefficient; + urgency_value += term * coefficient; // Tag- and project-specific coefficients. std::vector all; @@ -1044,7 +1053,7 @@ double Task::urgency () coefficient = context.config.getReal (*var); if (get ("project").find (project) == 0) - urgency += coefficient; + urgency_value += coefficient; } // urgency.user.tag..coefficient @@ -1055,7 +1064,7 @@ double Task::urgency () coefficient = context.config.getReal (*var); if (hasTag (tag)) - urgency += coefficient; + urgency_value += coefficient; } } } @@ -1066,10 +1075,11 @@ double Task::urgency () if (dependencyIsBlocking (*this)) term = 1.0; else term = 0.0; - urgency += term * coefficient; + urgency_value += term * coefficient; // Return the sum of all terms. - return urgency; + recalc_urgency = false; + return urgency_value; } //////////////////////////////////////////////////////////////////////////////// diff --git a/src/Task.h b/src/Task.h index a2eb89e00..57ade3974 100644 --- a/src/Task.h +++ b/src/Task.h @@ -52,6 +52,8 @@ public: // Public data. int id; + float urgency_value; + bool recalc_urgency; // Series of helper functions. static status textToStatus (const std::string&); @@ -82,7 +84,7 @@ public: void validate () const; - double urgency (); + float urgency (); private: int determineVersion (const std::string&); diff --git a/src/View.cpp b/src/View.cpp index bc8d50001..8da093b0a 100644 --- a/src/View.cpp +++ b/src/View.cpp @@ -26,6 +26,7 @@ //////////////////////////////////////////////////////////////////////////////// #include +#include #include #include #include @@ -43,8 +44,10 @@ View::View () , _extra_padding (0) , _extra_odd (0) , _extra_even (0) -, _truncate (0) +, _truncate_lines (0) +, _truncate_rows (0) , _lines (0) +, _rows (0) { } @@ -93,6 +96,8 @@ View::View () // std::string View::render (std::vector & data, std::vector & sequence) { + Timer timer ("View::render"); + // Determine minimal, ideal column widths. std::vector minimal; std::vector ideal; @@ -212,10 +217,14 @@ std::string View::render (std::vector & data, std::vector & sequence) } out += extra + "\n"; - ++_lines; + + // Stop if the line limit is exceeded. + if (++_lines >= _truncate_lines && _truncate_lines != 0) + return out; } // Compose, render columns, in sequence. + _rows = 0; std::vector > cells; std::vector ::iterator s; for (int s = 0; s < sequence.size (); ++s) @@ -247,7 +256,12 @@ std::string View::render (std::vector & data, std::vector & sequence) for (int c = 0; c < _columns.size (); ++c) { if (c) - out += (odd ? intra_odd : intra_even); + { + if (row_color.nontrivial ()) + out += row_color.colorize (intra); + else + out += (odd ? intra_odd : intra_even); + } if (i < cells[c].size ()) out += cells[c][i]; @@ -256,10 +270,17 @@ std::string View::render (std::vector & data, std::vector & sequence) } out += (odd ? extra_odd : extra_even) + "\n"; - ++_lines; + + // Stop if the line limit is exceeded. + if (++_lines >= _truncate_lines && _truncate_lines != 0) + return out; } cells.clear (); + + // Stop if the row limit is exceeded. + if (++_rows >= _truncate_rows && _truncate_rows != 0) + return out; } return out; diff --git a/src/View.h b/src/View.h index 293690de8..5745b9a3d 100644 --- a/src/View.h +++ b/src/View.h @@ -52,8 +52,10 @@ public: void extraPadding (int padding) { _extra_padding = padding; } void extraColorOdd (Color& c) { _extra_odd = c; } void extraColorEven (Color& c) { _extra_even = c; } - void truncate (int n) { _truncate = n; } + void truncateLines (int n) { _truncate_lines = n; } + void truncateRows (int n) { _truncate_rows = n; } int lines () { return _lines; } + int rows () { return _rows; } // View rendering. std::string render (std::vector &, std::vector &); @@ -71,8 +73,10 @@ private: int _extra_padding; Color _extra_odd; Color _extra_even; - int _truncate; + int _truncate_lines; + int _truncate_rows; int _lines; + int _rows; }; #endif diff --git a/src/columns/ColDate.cpp b/src/columns/ColDate.cpp index f271887c8..791dc014a 100644 --- a/src/columns/ColDate.cpp +++ b/src/columns/ColDate.cpp @@ -42,7 +42,6 @@ ColumnDate::ColumnDate () _style = "default"; _label = ""; _attribute = ""; - _report = ""; } //////////////////////////////////////////////////////////////////////////////// @@ -50,12 +49,6 @@ ColumnDate::~ColumnDate () { } -//////////////////////////////////////////////////////////////////////////////// -void ColumnDate::setReport (const std::string& report) -{ - _report = report; -} - //////////////////////////////////////////////////////////////////////////////// // Set the minimum and maximum widths for the value. void ColumnDate::measure (Task& task, int& minimum, int& maximum) @@ -97,7 +90,7 @@ void ColumnDate::measure (Task& task, int& minimum, int& maximum) else if (_style == "age") { Date now; - minimum = maximum = Duration (now - date).format ().length (); + minimum = maximum = Duration (now - date).formatCompact ().length (); } else throw std::string ("Unrecognized column format '") + _type + "." + _style + "'"; @@ -165,7 +158,7 @@ void ColumnDate::render ( lines.push_back ( color.colorize ( rightJustify ( - Duration (now - date).format (), width))); + Duration (now - date).formatCompact (), width))); } else if (_style == "short") { diff --git a/src/columns/ColDate.h b/src/columns/ColDate.h index 88092b08b..3a21dafcc 100644 --- a/src/columns/ColDate.h +++ b/src/columns/ColDate.h @@ -39,13 +39,11 @@ public: ColumnDate (); ~ColumnDate (); - void setReport (const std::string&); virtual void measure (Task&, int&, int&); virtual void render (std::vector &, Task&, int, Color&); protected: std::string _attribute; - std::string _report; }; #endif diff --git a/src/columns/Column.cpp b/src/columns/Column.cpp index 6cfc799fe..01ed55472 100644 --- a/src/columns/Column.cpp +++ b/src/columns/Column.cpp @@ -52,7 +52,7 @@ extern Context context; // // [.] // -Column* Column::factory (const std::string& name) +Column* Column::factory (const std::string& name, const std::string& report) { // Decompose name into type and style. std::string::size_type dot = name.find ('.'); @@ -89,33 +89,8 @@ Column* Column::factory (const std::string& name) else throw std::string ("Unrecognized column type '") + column_name + "'"; + column->setReport (report); column->setStyle (column_style); - -/* - // TODO Load the report column def from config - // TODO Parse column defs - // TODO Create column object - // TODO Column: name - // TODO Column: style - // TODO Column: break - - // TODO Color: odd - // TODO Color: even - // TODO Color: intra_odd - // TODO Color: intra_even - // TODO Color: extra_odd - // TODO Color: extra_even - // TODO Color: header - - // Terminal width. - view.width (getWidth ()); - - // TODO Intra padding. - // TODO Extra padding. - // TODO Margin. - // TODO Truncate lines/page. -*/ - return column; } @@ -124,6 +99,7 @@ Column::Column () : _type ("string") , _style ("default") , _label ("") +, _report ("") { } diff --git a/src/columns/Column.h b/src/columns/Column.h index 3e06866d5..75853a4db 100644 --- a/src/columns/Column.h +++ b/src/columns/Column.h @@ -35,7 +35,7 @@ class Column { public: - static Column* factory (const std::string&); + static Column* factory (const std::string&, const std::string&); Column (); Column (const Column&); @@ -47,8 +47,9 @@ public: std::string getLabel () { return _label; } std::string type () const { return _type; } - virtual void setStyle (const std::string& value) { _style = value; } - virtual void setLabel (const std::string& value) { _label = value; } + virtual void setStyle (const std::string& value) { _style = value; } + virtual void setLabel (const std::string& value) { _label = value; } + virtual void setReport (const std::string& value) { _report = value; } virtual void measure (Task&, int&, int&) = 0; virtual void renderHeader (std::vector &, int, Color&); @@ -58,6 +59,7 @@ protected: std::string _type; std::string _style; std::string _label; + std::string _report; }; #endif diff --git a/src/command.cpp b/src/command.cpp index d556fdd5d..5bc87142b 100644 --- a/src/command.cpp +++ b/src/command.cpp @@ -1102,7 +1102,7 @@ int handleShow (std::string& outs) "color.calendar.due color.calendar.due.today color.calendar.overdue regex " "color.calendar.weekend color.calendar.holiday color.calendar.weeknumber " "color.summary.background color.summary.bar color.history.add " - "color.history.done color.history.delete color.undo.before " + "color.history.done color.history.delete color.undo.before color.label " "color.sync.added color.sync.changed color.sync.rejected color.undo.after " "confirmation data.location dateformat dateformat.holiday " "dateformat.report dateformat.annotation debug default.command default.due " @@ -1116,7 +1116,8 @@ int handleShow (std::string& outs) "active.indicator tag.indicator recurrence.indicator recurrence.limit " "list.all.projects list.all.tags undo.style verbose rule.precedence.color " "merge.autopush merge.default.uri pull.default.uri push.default.uri " - "xterm.title shell.prompt " + "xterm.title shell.prompt indent.annotation indent.report column.spacing " + "row.padding column.padding " "import.synonym.status import.synonym.tags import.synonym.entry " "import.synonym.start import.synonym.due import.synonym.recur " "import.synonym.end import.synonym.project import.synonym.priority " diff --git a/src/custom.cpp b/src/custom.cpp index ddf9838fb..05e36e4bb 100644 --- a/src/custom.cpp +++ b/src/custom.cpp @@ -35,728 +35,97 @@ #include #include -#include "Context.h" -#include "Date.h" -#include "Duration.h" -#include "Table.h" -#include "text.h" -#include "util.h" -#include "main.h" +#include +#include +#include +#include +#include +#include +#include +#include extern Context context; -static std::vector customReports; //////////////////////////////////////////////////////////////////////////////// -// This report will eventually become the one report that many others morph into -// via the .taskrc file. -int handleCustomReport (const std::string& report, std::string& outs) +void validateReportColumns (std::vector & columns) { - int rc = 0; - - // Load report configuration. - std::string reportColumns = context.config.get ("report." + report + ".columns"); - std::string reportLabels = context.config.get ("report." + report + ".labels"); - std::string reportSort = context.config.get ("report." + report + ".sort"); - std::string reportFilter = context.config.get ("report." + report + ".filter"); - - std::vector columns; - split (columns, reportColumns, ','); - validReportColumns (columns); - - std::vector labels; - split (labels, reportLabels, ','); - - if (columns.size () != labels.size () && labels.size () != 0) - throw std::string ("There are a different number of columns and labels ") + - "for report '" + report + "'."; - - std::map columnLabels; - if (labels.size ()) - for (unsigned int i = 0; i < columns.size (); ++i) - columnLabels[columns[i]] = labels[i]; - - std::vector sortOrder; - split (sortOrder, reportSort, ','); - validSortColumns (columns, sortOrder); - - // Apply rc overrides. - std::vector filterArgs; - std::vector filteredArgs; - split (filterArgs, reportFilter, ' '); - context.applyOverrides (filterArgs, filteredArgs); - + // One-time initialization, on demand. + static std::map legacyMap; + if (! legacyMap.size ()) { - Cmd cmd (report); - Task task; - Sequence sequence; - Subst subst; - Filter filter; - context.parse (filteredArgs, cmd, task, sequence, subst, filter); - - context.sequence.combine (sequence); - - // Special case: Allow limit to be overridden by the command line. - if (!context.task.has ("limit") && task.has ("limit")) - context.task.set ("limit", task.get ("limit")); - - foreach (att, filter) - context.filter.push_back (*att); + legacyMap["priority_long"] = "priority.long"; + legacyMap["entry_time"] = "entry"; + legacyMap["start_time"] = "start"; + legacyMap["end_time"] = "end"; + legacyMap["countdown"] = "due.countdown"; + legacyMap["countdown_compact"] = "due.countdown"; + legacyMap["age"] = "entry.age"; + legacyMap["age_compact"] = "entry.age"; + legacyMap["active"] = "start.active"; + legacyMap["recurrence_indicator"] = "recur.indicator"; + legacyMap["tag_indicator"] = "tags.indicator"; + legacyMap["description_only"] = "description.desc"; } - // Get all the tasks. - std::vector tasks; - context.tdb.lock (context.config.getBoolean ("locking")); - handleRecurrence (); - context.tdb.load (tasks, context.filter); - context.tdb.commit (); - context.tdb.unlock (); - - // Filter sequence. - if (context.sequence.size ()) - context.filter.applySequence (tasks, context.sequence); - - // Determine the output date format, which uses a hierarchy of definitions. - std::string dateformat = context.config.get ("report." + report + ".dateformat"); - if (dateformat == "") - dateformat = context.config.get ("dateformat.report"); - if (dateformat == "") - dateformat = context.config.get ("dateformat"); - - Table table; - table.setTableWidth (context.getWidth ()); - table.setDateFormat (dateformat); - table.setReportName (report); - - foreach (task, tasks) - table.addRow (); - - int columnCount = 0; - int dueColumn = -1; - foreach (col, columns) + std::vector ::iterator i; + for (i = columns.begin (); i != columns.end (); ++i) { - // Add each column individually. - if (*col == "id") + // If a legacy column was used, complain about it, but modify it anyway. + std::map ::iterator found = legacyMap.find (*i); + if (found != legacyMap.end ()) { - table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "ID"); - table.setColumnWidth (columnCount, Table::minimum); - table.setColumnJustification (columnCount, Table::right); - - std::string value; - int row = 0; - foreach (task, tasks) - { - if (task->id != 0) - value = format (task->id); - else - value = "-"; - - table.addCell (row++, columnCount, value); - } + context.footnote (std::string ("Deprecated report field '") + + *i + + "' used. Please modify this to '" + + found->second + + "'."); + *i = found->second; } - - else if (*col == "uuid") - { - table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "UUID"); - table.setColumnWidth (columnCount, Table::minimum); - table.setColumnJustification (columnCount, Table::left); - - std::string value; - int row = 0; - foreach (task, tasks) - { - value = task->get ("uuid"); - table.addCell (row++, columnCount, value); - } - } - - else if (*col == "project") - { - table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "Project"); - table.setColumnWidth (columnCount, Table::minimum); - table.setColumnJustification (columnCount, Table::left); - - std::string value; - int row = 0; - foreach (task, tasks) - { - value = task->get ("project"); - table.addCell (row++, columnCount, value); - } - } - - else if (*col == "priority") - { - table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "Pri"); - table.setColumnWidth (columnCount, Table::minimum); - table.setColumnJustification (columnCount, Table::left); - - int row = 0; - foreach (task, tasks) - { - std::string value = task->get ("priority"); - table.addCell (row++, columnCount, value); - } - } - - else if (*col == "priority_long") - { - table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "Pri"); - table.setColumnWidth (columnCount, Table::minimum); - table.setColumnJustification (columnCount, Table::left); - - int row = 0; - std::string pri; - foreach (task, tasks) - { - pri = task->get ("priority"); - - if (pri == "H") pri = "High"; // TODO i18n - else if (pri == "M") pri = "Medium"; // TODO i18n - else if (pri == "L") pri = "Low"; // TODO i18n - - table.addCell (row++, columnCount, pri); - } - } - - else if (*col == "entry" || *col == "entry_time") - { - table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "Added"); - table.setColumnWidth (columnCount, Table::minimum); - table.setColumnJustification (columnCount, Table::right); - - std::string entered; - for (unsigned int row = 0; row < tasks.size(); ++row) - { - entered = tasks[row].get ("entry"); - if (entered.length ()) - { - Date dt (::atoi (entered.c_str ())); - entered = dt.toString (dateformat); - table.addCell (row, columnCount, entered); - } - } - } - - else if (*col == "start" || *col == "start_time") - { - table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "Started"); - table.setColumnWidth (columnCount, Table::minimum); - table.setColumnJustification (columnCount, Table::right); - - std::string started; - for (unsigned int row = 0; row < tasks.size(); ++row) - { - started = tasks[row].get ("start"); - if (started.length ()) - { - Date dt (::atoi (started.c_str ())); - started = dt.toString (dateformat); - table.addCell (row, columnCount, started); - } - } - } - - else if (*col == "end" || *col == "end_time") - { - table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "Completed"); - table.setColumnWidth (columnCount, Table::minimum); - table.setColumnJustification (columnCount, Table::right); - - std::string ended; - for (unsigned int row = 0; row < tasks.size(); ++row) - { - ended = tasks[row].get ("end"); - if (ended.length ()) - { - Date dt (::atoi (ended.c_str ())); - ended = dt.toString (dateformat); - table.addCell (row, columnCount, ended); - } - } - } - - else if (*col == "due") - { - table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "Due"); - table.setColumnWidth (columnCount, Table::minimum); - table.setColumnJustification (columnCount, Table::left); - - int row = 0; - std::string due; - foreach (task, tasks) - { - std::string value = getDueDate (*task, dateformat); - table.addCell (row++, columnCount, value); - } - - dueColumn = columnCount; - } - - else if (*col == "countdown") - { - table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "Countdown"); - table.setColumnWidth (columnCount, Table::minimum); - table.setColumnJustification (columnCount, Table::right); - - std::string due; - std::string countdown; - Date now; - for (unsigned int row = 0; row < tasks.size(); ++row) - { - due = tasks[row].get ("due"); - if (due.length ()) - { - Date dt (::atoi (due.c_str ())); - countdown = Duration (now - dt).format (); - table.addCell (row, columnCount, countdown); - } - } - } - - else if (*col == "countdown_compact") - { - table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "Countdown"); - table.setColumnWidth (columnCount, Table::minimum); - table.setColumnJustification (columnCount, Table::right); - - std::string due; - std::string countdown; - Date now; - for (unsigned int row = 0; row < tasks.size(); ++row) - { - due = tasks[row].get ("due"); - if (due.length ()) - { - Date dt (::atoi (due.c_str ())); - countdown = Duration (now - dt).formatCompact (); - table.addCell (row, columnCount, countdown); - } - } - } - - else if (*col == "age") - { - table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "Age"); - table.setColumnWidth (columnCount, Table::minimum); - table.setColumnJustification (columnCount, Table::right); - - std::string created; - std::string age; - Date now; - for (unsigned int row = 0; row < tasks.size(); ++row) - { - created = tasks[row].get ("entry"); - if (created.length ()) - { - Date dt (::atoi (created.c_str ())); - age = Duration (now - dt).format (); - table.addCell (row, columnCount, age); - } - } - } - - else if (*col == "age_compact") - { - table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "Age"); - table.setColumnWidth (columnCount, Table::minimum); - table.setColumnJustification (columnCount, Table::right); - - std::string created; - std::string age; - Date now; - for (unsigned int row = 0; row < tasks.size(); ++row) - { - created = tasks[row].get ("entry"); - if (created.length ()) - { - Date dt (::atoi (created.c_str ())); - age = Duration (now - dt).formatCompact (); - table.addCell (row, columnCount, age); - } - } - } - - else if (*col == "active") - { - table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "Active"); - table.setColumnWidth (columnCount, Table::minimum); - table.setColumnJustification (columnCount, Table::left); - - for (unsigned int row = 0; row < tasks.size(); ++row) - if (tasks[row].has ("start")) - table.addCell (row, columnCount, context.config.get ("active.indicator")); - } - - else if (*col == "tags") - { - table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "Tags"); - table.setColumnWidth (columnCount, Table::minimum); - table.setColumnJustification (columnCount, Table::left); - - int row = 0; - std::vector all; - std::string tags; - foreach (task, tasks) - { - task->getTags (all); - join (tags, " ", all); - table.addCell (row++, columnCount, tags); - } - } - - else if (*col == "description_only") - { - table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "Description"); - table.setColumnWidth (columnCount, Table::flexible); - table.setColumnJustification (columnCount, Table::left); - - std::string desc; - int row = 0; - foreach (task, tasks) - { - desc = task->get ("description"); - table.addCell (row++, columnCount, desc); - } - } - - else if (*col == "description") - { - table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "Description"); - table.setColumnWidth (columnCount, Table::flexible); - table.setColumnJustification (columnCount, Table::left); - - std::string desc; - int row = 0; - foreach (task, tasks) - { - desc = getFullDescription (*task, report); - table.addCell (row++, columnCount, desc); - } - } - - else if (*col == "recur") - { - table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "Recur"); - table.setColumnWidth (columnCount, Table::minimum); - table.setColumnJustification (columnCount, Table::right); - - for (unsigned int row = 0; row < tasks.size(); ++row) - { - std::string recur = tasks[row].get ("recur"); - if (recur != "") - { - table.addCell (row, columnCount, recur); - } - } - } - - else if (*col == "recurrence_indicator") - { - table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "R"); - table.setColumnWidth (columnCount, Table::minimum); - table.setColumnJustification (columnCount, Table::right); - - for (unsigned int row = 0; row < tasks.size(); ++row) - if (tasks[row].has ("recur")) - table.addCell (row, columnCount, context.config.get ("recurrence.indicator")); - } - - else if (*col == "tag_indicator") - { - table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "T"); - table.setColumnWidth (columnCount, Table::minimum); - table.setColumnJustification (columnCount, Table::right); - - for (unsigned int row = 0; row < tasks.size(); ++row) - if (tasks[row].getTagCount ()) - table.addCell (row, columnCount, context.config.get ("tag.indicator")); - } - - else if (*col == "wait") - { - table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "Wait"); - table.setColumnWidth (columnCount, Table::minimum); - table.setColumnJustification (columnCount, Table::right); - - int row = 0; - std::string wait; - foreach (task, tasks) - { - wait = task->get ("wait"); - if (wait != "") - { - Date dt (::atoi (wait.c_str ())); - wait = dt.toString (dateformat); - table.addCell (row++, columnCount, wait); - } - } - } - - else if (*col == "depends") - { - table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "Deps"); - table.setColumnWidth (columnCount, Table::minimum); - table.setColumnJustification (columnCount, Table::right); - - int row = 0; - std::vector blocked; - std::vector blocked_ids; - std::string deps; - foreach (task, tasks) - { - dependencyGetBlocking (*task, blocked); - foreach (b, blocked) - blocked_ids.push_back (b->id); - - join (deps, ",", blocked_ids); - blocked_ids.clear (); - blocked.clear (); - - table.addCell (row++, columnCount, deps); - } - } - - else if (*col == "urgency") - { - table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "Urgency"); - table.setColumnWidth (columnCount, Table::minimum); - table.setColumnJustification (columnCount, Table::right); - - int row = 0; - foreach (task, tasks) - { - std::string value = format (task->urgency (), 4, 3); - table.addCell (row++, columnCount, value); - } - } - - else if (*col == "status") - { - table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "Status"); - table.setColumnWidth (columnCount, Table::minimum); - table.setColumnJustification (columnCount, Table::left); - - int row = 0; - foreach (task, tasks) - { - table.addCell (row++, columnCount, task->statusToText (task->getStatus ())); - } - } - - // Common to all columns. - // Add underline. - if (context.color () && context.config.getBoolean ("fontunderline")) - table.setColumnUnderline (columnCount); - else - table.setTableDashedUnderline (); - - ++columnCount; - } - - // Dynamically add sort criteria. - // Build a map of column names -> index. - std::map columnIndex; - for (unsigned int c = 0; c < columns.size (); ++c) - columnIndex[columns[c]] = c; - - foreach (sortColumn, sortOrder) - { - // Separate column and direction. - std::string column = sortColumn->substr (0, sortColumn->length () - 1); - char direction = (*sortColumn)[sortColumn->length () - 1]; - - // TODO This code should really be using Att::type. - if (column == "id" || column == "urgency") - table.sortOn (columnIndex[column], - (direction == '+' ? - Table::ascendingNumeric : - Table::descendingNumeric)); - - else if (column == "priority") - table.sortOn (columnIndex[column], - (direction == '+' ? - Table::ascendingPriority : - Table::descendingPriority)); - - else if (column == "entry" || column == "start" || column == "wait" || - column == "until" || column == "end" || column == "entry_time" || - column == "start_time" || column == "end_time") - table.sortOn (columnIndex[column], - (direction == '+' ? - Table::ascendingDate : - Table::descendingDate)); - - else if (column == "due") - table.sortOn (columnIndex[column], - (direction == '+' ? - Table::ascendingDueDate : - Table::descendingDueDate)); - - else if (column == "recur" || column == "age" || column == "age_compact") - table.sortOn (columnIndex[column], - (direction == '+' ? - Table::ascendingPeriod : - Table::descendingPeriod)); - - else if (column == "countdown" || column == "countdown_compact") - table.sortOn (columnIndex[column], - (direction == '+' ? - Table::descendingPeriod : // Yes, these are flipped. - Table::ascendingPeriod)); // Yes, these are flipped. - - else - table.sortOn (columnIndex[column], - (direction == '+' ? - Table::ascendingCharacter : - Table::descendingCharacter)); - } - - // Now auto colorize all rows. - if (context.color ()) - { - for (unsigned int row = 0; row < tasks.size (); ++row) - { - Color c (tasks[row].get ("fg") + " " + tasks[row].get ("bg")); - autoColorize (tasks[row], c); - table.setRowColor (row, c); - } - } - - // If an alternating row color is specified, notify the table. - if (context.color ()) - { - Color alternate (context.config.get ("color.alternate")); - if (alternate.nontrivial ()) - table.setTableAlternateColor (alternate); - } - - // How many lines taken up by table header? - int table_header; - if (context.color () && context.config.getBoolean ("fontunderline")) - table_header = 1; // Underlining doesn't use extra line. - else - table_header = 2; // Dashes use an extra line. - - // Report output can be limited by rows or lines. - int maxrows = 0; - int maxlines = 0; - getLimits (report, maxrows, maxlines); - - // Adjust for fluff in the output. - if (maxlines) - maxlines -= (context.config.getBoolean ("blanklines") ? 1 : 0) - + table_header - + context.headers.size () - + context.footnotes.size (); - - std::stringstream out; - if (table.rowCount ()) - { - out << optionalBlankLine () - << table.render (maxrows, maxlines) - << optionalBlankLine () - << table.rowCount () - << (table.rowCount () == 1 ? " task" : " tasks"); - - if (maxrows && maxrows < table.rowCount ()) - out << ", " << maxrows << " shown"; - - if (maxlines && maxlines < table.rowCount ()) - out << ", truncated to " << maxlines - table_header << " lines"; - - out << std::endl; - } - else - { - out << "No matches." - << std::endl; - rc = 1; - } - - outs = out.str (); - return rc; -} - -//////////////////////////////////////////////////////////////////////////////// -void validReportColumns (const std::vector & columns) -{ - std::vector bad; - - std::vector ::const_iterator it; - for (it = columns.begin (); it != columns.end (); ++it) - if (*it != "id" && - *it != "uuid" && - *it != "project" && - *it != "priority" && - *it != "priority_long" && - *it != "entry" && - *it != "entry_time" && // TODO Deprecated - *it != "start" && - *it != "start_time" && // TODO Deprecated - *it != "end" && - *it != "end_time" && // TODO Deprecated - *it != "due" && - *it != "countdown" && - *it != "countdown_compact" && - *it != "age" && - *it != "age_compact" && - *it != "active" && - *it != "tags" && - *it != "recur" && - *it != "recurrence_indicator" && - *it != "tag_indicator" && - *it != "description_only" && - *it != "description" && - *it != "wait" && - *it != "depends" && - *it != "urgency" && - *it != "status") - bad.push_back (*it); - - if (bad.size ()) - { - std::string error; - join (error, ", ", bad); - throw std::string ("Unrecognized column name: ") + error + "."; } } //////////////////////////////////////////////////////////////////////////////// -void validSortColumns ( - const std::vector & columns, - const std::vector & sortColumns) +void validateSortColumns (std::vector & columns) { - std::vector bad; - std::vector ::const_iterator sc; - for (sc = sortColumns.begin (); sc != sortColumns.end (); ++sc) + // One-time initialization, on demand. + static std::map legacyMap; + if (! legacyMap.size ()) { - char direction = (*sc)[sc->length () - 1]; - if (direction != '-' && direction != '+') - throw std::string ("Sort column '") + - *sc + - "' does not have a +/- ascending/descending indicator."; - - std::vector ::const_iterator co; - for (co = columns.begin (); co != columns.end (); ++co) - if (sc->substr (0, sc->length () - 1) == *co) - break; - - if (co == columns.end ()) - bad.push_back (*sc); + legacyMap["priority_long"] = "priority"; + legacyMap["entry_time"] = "entry"; + legacyMap["start_time"] = "start"; + legacyMap["end_time"] = "end"; + legacyMap["countdown"] = "due"; + legacyMap["countdown_compact"] = "due"; + legacyMap["age"] = "entry"; + legacyMap["age_compact"] = "entry"; + legacyMap["active"] = "start"; + legacyMap["recurrence_indicator"] = "recur"; + legacyMap["tag_indicator"] = "tags"; + legacyMap["description_only"] = "description"; } - if (bad.size ()) + std::vector ::iterator i; + for (i = columns.begin (); i != columns.end (); ++i) { - std::string error; - join (error, ", ", bad); - throw std::string ("Sort column is not part of the report: ") + error + "."; + // If a legacy column was used, complain about it, but modify it anyway. + std::map ::iterator found = legacyMap.find (*i); + if (found != legacyMap.end ()) + { + context.footnote (std::string ("Deprecated sort field '") + + *i + + "' used. Please modify this to '" + + found->second + + "'."); + *i = found->second; + } } } //////////////////////////////////////////////////////////////////////////////// // A value of zero mean unlimited. // A value of 'page' means however many screen lines there are. -// A value of a positive integer is a row limit. +// A value of a positive integer is a row/task limit. void getLimits (const std::string& report, int& rows, int& lines) { rows = 0; @@ -795,3 +164,146 @@ void getLimits (const std::string& report, int& rows, int& lines) } //////////////////////////////////////////////////////////////////////////////// +int handleCustomReport (const std::string& report, std::string& outs) +{ + int rc = 0; + + // Load report configuration. + std::string reportColumns = context.config.get ("report." + report + ".columns"); + std::string reportLabels = context.config.get ("report." + report + ".labels"); + std::string reportSort = context.config.get ("report." + report + ".sort"); + std::string reportFilter = context.config.get ("report." + report + ".filter"); + + std::vector columns; + split (columns, reportColumns, ','); + validateReportColumns (columns); + + std::vector labels; + split (labels, reportLabels, ','); + + if (columns.size () != labels.size () && labels.size () != 0) + throw std::string ("There are a different number of columns and labels ") + + "for report '" + report + "'."; + + std::map columnLabels; + if (labels.size ()) + for (unsigned int i = 0; i < columns.size (); ++i) + columnLabels[columns[i]] = labels[i]; + + std::vector sortOrder; + split (sortOrder, reportSort, ','); + validateSortColumns (sortOrder); + + // Apply rc overrides. + std::vector filterArgs; + std::vector filteredArgs; + split (filterArgs, reportFilter, ' '); + context.applyOverrides (filterArgs, filteredArgs); + + { + Cmd cmd (report); + Task task; + Sequence sequence; + Subst subst; + Filter filter; + context.parse (filteredArgs, cmd, task, sequence, subst, filter); + + context.sequence.combine (sequence); + + // Special case: Allow limit to be overridden by the command line. + if (!context.task.has ("limit") && task.has ("limit")) + context.task.set ("limit", task.get ("limit")); + + foreach (att, filter) + context.filter.push_back (*att); + } + + // Get all the tasks. + std::vector tasks; + context.tdb.lock (context.config.getBoolean ("locking")); + handleRecurrence (); + context.tdb.load (tasks, context.filter); + context.tdb.commit (); + context.tdb.unlock (); + + // Filter sequence. + if (context.sequence.size ()) + context.filter.applySequence (tasks, context.sequence); + + // Sort the tasks. + std::vector sequence; + for (int i = 0; i < tasks.size (); ++i) + sequence.push_back (i); + + sort_tasks (tasks, sequence, reportSort); + + // Configure the view. + View view; + view.width (context.getWidth ()); + view.leftMargin (context.config.getInteger ("indent.report")); + view.extraPadding (context.config.getInteger ("row.padding")); + view.intraPadding (context.config.getInteger ("column.padding")); + + Color label (context.config.get ("color.label")); + view.colorHeader (label); + + Color alternate (context.config.get ("color.alternate")); + view.colorOdd (alternate); + view.intraColorOdd (alternate); + + // Add the columns. + std::vector ::iterator it; + for (it = columns.begin (); it != columns.end (); ++it) + view.add (Column::factory (*it, report)); + + // How many lines taken up by table header? + int table_header; + if (context.color () && context.config.getBoolean ("fontunderline")) + table_header = 1; // Underlining doesn't use extra line. + else + table_header = 2; // Dashes use an extra line. + + // Report output can be limited by rows or lines. + int maxrows = 0; + int maxlines = 0; + getLimits (report, maxrows, maxlines); + + // Adjust for fluff in the output. + if (maxlines) + maxlines -= (context.config.getBoolean ("blanklines") ? 1 : 0) + + table_header + + context.headers.size () + + context.footnotes.size (); + + std::stringstream out; + if (tasks.size ()) + { + view.truncateRows (maxrows); + view.truncateLines (maxlines); + + out << optionalBlankLine () + << view.render (tasks, sequence) + << optionalBlankLine () + << tasks.size () + << (tasks.size () == 1 ? " task" : " tasks"); + + if (maxrows && maxrows < view.rows ()) + out << ", " << maxrows << " shown"; + + if (maxlines && maxlines < view.rows ()) + out << ", truncated to " << maxlines - table_header << " lines"; + + out << "\n"; + } + else + { + out << "No matches." + << std::endl; + rc = 1; + } + + outs = out.str (); + return rc; +} + +//////////////////////////////////////////////////////////////////////////////// diff --git a/src/main.h b/src/main.h index 4c7eaa811..262d3042c 100644 --- a/src/main.h +++ b/src/main.h @@ -125,8 +125,8 @@ int handleReportGHistoryAnnual (std::string&); // custom.cpp int handleCustomReport (const std::string&, std::string&); -void validReportColumns (const std::vector &); -void validSortColumns (const std::vector &, const std::vector &); +void validateReportColumns (const std::vector &); +void validateSortColumns (const std::vector &); void getLimits (const std::string&, int&, int&); // rules.cpp diff --git a/src/report.cpp b/src/report.cpp index 32f3e54d6..27f965991 100644 --- a/src/report.cpp +++ b/src/report.cpp @@ -634,10 +634,9 @@ int handleInfo (std::string& outs) } // Task::urgency - // TODO Enable this later. This was for testing. - //row = table.addRow (); - //table.addCell (row, 0, "Urgency"); - //table.addCell (row, 1, task->urgency ()); + row = table.addRow (); + table.addCell (row, 0, "Urgency"); + table.addCell (row, 1, task->urgency ()); // Create a second table, containing undo log change details. Table journal; diff --git a/src/sort.cpp b/src/sort.cpp index 37eb6a6be..1e9b534cf 100644 --- a/src/sort.cpp +++ b/src/sort.cpp @@ -31,6 +31,7 @@ #include #include #include +#include #include extern Context context; @@ -45,6 +46,8 @@ void sort_tasks ( std::vector & order, const std::string& keys) { + Timer t ("Sort"); + global_data = &data; // Split the key defs. diff --git a/test/view.t.cpp b/test/view.t.cpp index 972542552..f38c6b107 100644 --- a/test/view.t.cpp +++ b/test/view.t.cpp @@ -99,33 +99,34 @@ int main (int argc, char** argv) Color even_color ("on gray0"); // Create a view. + std::string report = "view.t"; View view; - view.add (Column::factory ("id")); - view.add (Column::factory ("uuid.short")); - view.add (Column::factory ("project")); - view.add (Column::factory ("priority.long")); - view.add (Column::factory ("tags")); -// view.add (Column::factory ("tags.indicator")); - view.add (Column::factory ("tags.count")); - view.add (Column::factory ("description")); -// view.add (Column::factory ("description.desc")); -// view.add (Column::factory ("description.truncated")); -// view.add (Column::factory ("description.oneline")); -// view.add (Column::factory ("description.count")); -// view.add (Column::factory ("depends")); -// view.add (Column::factory ("depends.count")); - view.add (Column::factory ("depends.indicator")); -// view.add (Column::factory ("recur")); - view.add (Column::factory ("recur.indicator")); -// view.add (Column::factory ("status")); - view.add (Column::factory ("status.short")); -// view.add (Column::factory ("due")); -// view.add (Column::factory ("due.julian")); - view.add (Column::factory ("due.countdown")); -// view.add (Column::factory ("due.epoch")); -// view.add (Column::factory ("due.iso")); - view.add (Column::factory ("start.active")); - view.add (Column::factory ("urgency")); + view.add (Column::factory ("id", report)); + view.add (Column::factory ("uuid.short", report)); + view.add (Column::factory ("project", report)); + view.add (Column::factory ("priority.long", report)); + view.add (Column::factory ("tags", report)); +// view.add (Column::factory ("tags.indicator", report)); + view.add (Column::factory ("tags.count", report)); + view.add (Column::factory ("description", report)); +// view.add (Column::factory ("description.desc", report)); +// view.add (Column::factory ("description.truncated", report)); +// view.add (Column::factory ("description.oneline", report)); +// view.add (Column::factory ("description.count", report)); +// view.add (Column::factory ("depends", report)); +// view.add (Column::factory ("depends.count", report)); + view.add (Column::factory ("depends.indicator", report)); +// view.add (Column::factory ("recur", report)); + view.add (Column::factory ("recur.indicator", report)); +// view.add (Column::factory ("status", report)); + view.add (Column::factory ("status.short", report)); +// view.add (Column::factory ("due", report)); +// view.add (Column::factory ("due.julian", report)); + view.add (Column::factory ("due.countdown", report)); +// view.add (Column::factory ("due.epoch", report)); +// view.add (Column::factory ("due.iso", report)); + view.add (Column::factory ("start.active", report)); + view.add (Column::factory ("urgency", report)); view.width (context.getWidth ()); view.leftMargin (4); /*