mirror of
https://github.com/GothenburgBitFactory/taskwarrior.git
synced 2025-06-26 10:54:26 +02:00
Enhancement - annotations
- Added support for "annotate" command to annotate existing tasks. - Bumped file format to version 3, due to the annotations. - Added unit tests to verify that annotations work. - Changed 'description' column everywhere to include annotations. - Added 'description_only' column to exclude the annotations. - Fixed bug in Table.cpp that calculated the width of multi-line columns by using the cell length, instead of the length of the longest individual line. - Updated documentation with new feature. - Updated documentation with new column. - Enhanced t.t unit tests to cover format 43
This commit is contained in:
parent
ca795ea281
commit
3979c3283e
16 changed files with 446 additions and 54 deletions
|
@ -12,6 +12,10 @@
|
|||
+ Added support for the "echo.command" configuration variable that displays
|
||||
the task affected by the start, stop, do, undo, delete and undelete
|
||||
commands (thanks to Bruce Dillahunty).
|
||||
+ Added support for task annotations, with each annotation comprising a
|
||||
timestamp and a description.
|
||||
+ Added support for a 'description_only' column that can be used in custom
|
||||
reports which excludes annotations.
|
||||
|
||||
------ old releases ------------------------------
|
||||
|
||||
|
|
|
@ -102,6 +102,25 @@ Car 2 2 wks 25% XXXXXXXXX</code></pre>
|
|||
Appends the additional description to an existing task.
|
||||
</p>
|
||||
|
||||
<strong>% task annotate <id> additional note...</strong>
|
||||
<p>
|
||||
Allows an annotation to be attached to an existing task. Each
|
||||
annotation has a time stamp, and when displayed, the annotations
|
||||
are shown under the task description. For example:
|
||||
</p>
|
||||
|
||||
<pre><code>% task add Go to the supermarket
|
||||
% task annotate 1 need milk
|
||||
% task ls
|
||||
|
||||
ID Project Pri Due Active Age Description
|
||||
1 Go to the supermarket
|
||||
3/23/2009 need milk</code></pre>
|
||||
<p>
|
||||
The date of the annotation uses the "dateformat" configuration
|
||||
variable.
|
||||
</p>
|
||||
|
||||
<strong>% task delete <id></strong>
|
||||
<p>
|
||||
There are two ways of getting rid of tasks - mark them as done, or
|
||||
|
|
|
@ -100,6 +100,7 @@ report.mine.sort=priority-,project+</pre></code>
|
|||
<li>active
|
||||
<li>tags
|
||||
<li>recur
|
||||
<li>description_only
|
||||
<li>description
|
||||
</ul>
|
||||
|
||||
|
|
|
@ -108,6 +108,10 @@
|
|||
<li>Added support for the "echo.command" configuration variable that displays
|
||||
the task affected by the start, stop, do, undo, delete and undelete
|
||||
commands (thanks to Bruce Dillahunty).
|
||||
<li>Added support for task annotations, with each annotation comprising a
|
||||
timestamp and a description.
|
||||
<li>Added support for a 'description_only' column that can be used in custom
|
||||
reports which excludes annotations.
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
|
|
|
@ -37,6 +37,7 @@
|
|||
<pre><code>Usage: task
|
||||
task add [tags] [attrs] desc...
|
||||
task append [tags] [attrs] desc...
|
||||
task annotate ID desc...
|
||||
task completed [tags] [attrs] desc...
|
||||
task ID [tags] [attrs] [desc...]
|
||||
task ID /from/to/
|
||||
|
|
|
@ -171,6 +171,7 @@ void Config::createDefault (const std::string& home)
|
|||
|
||||
// Custom reports.
|
||||
fprintf (out, "# Fields: id,uuid,project,priority,entry,start,due,recur,age,active,tags,description\n");
|
||||
fprintf (out, "# description_only\n");
|
||||
fprintf (out, "# Description: This report is ...\n");
|
||||
fprintf (out, "# Sort: due+,priority-,project+\n");
|
||||
fprintf (out, "# Filter: pro:x pri:H +bug\n");
|
||||
|
|
169
src/T.cpp
169
src/T.cpp
|
@ -25,6 +25,7 @@
|
|||
//
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <algorithm>
|
||||
#include "task.h"
|
||||
#include "T.h"
|
||||
|
@ -39,6 +40,7 @@ T::T ()
|
|||
mTags.clear ();
|
||||
mAttributes.clear ();
|
||||
mDescription = "";
|
||||
mAnnotations.clear ();
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -58,6 +60,7 @@ T::T (const T& other)
|
|||
mTags = other.mTags;
|
||||
mRemoveTags = other.mRemoveTags;
|
||||
mAttributes = other.mAttributes;
|
||||
mAnnotations = other.mAnnotations;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -72,6 +75,7 @@ T& T::operator= (const T& other)
|
|||
mTags = other.mTags;
|
||||
mRemoveTags = other.mRemoveTags;
|
||||
mAttributes = other.mAttributes;
|
||||
mAnnotations = other.mAnnotations;
|
||||
}
|
||||
|
||||
return *this;
|
||||
|
@ -244,7 +248,23 @@ void T::setSubstitution (const std::string& from, const std::string& to)
|
|||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// uuid status [tags] [attributes] description
|
||||
void T::getAnnotations (std::map <time_t, std::string>& all)
|
||||
{
|
||||
all = mAnnotations;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
void T::addAnnotation (const std::string& description)
|
||||
{
|
||||
std::string sanitized = description;
|
||||
std::replace (sanitized.begin (), sanitized.end (), '\'', '"');
|
||||
std::replace (sanitized.begin (), sanitized.end (), '[', '(');
|
||||
std::replace (sanitized.begin (), sanitized.end (), ']', ')');
|
||||
mAnnotations[time (NULL)] = sanitized;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// uuid status [tags] [attributes] [annotations] description
|
||||
//
|
||||
// uuid \x{8}-\x{4}-\x{4}-\x{4}-\x{12}
|
||||
// status - + X r
|
||||
|
@ -282,10 +302,26 @@ const std::string T::compose () const
|
|||
++count;
|
||||
}
|
||||
|
||||
line += "] ";
|
||||
line += "] [";
|
||||
|
||||
// Annotations
|
||||
std::stringstream annotation;
|
||||
bool first = true;
|
||||
foreach (note, mAnnotations)
|
||||
{
|
||||
if (first)
|
||||
first = false;
|
||||
else
|
||||
annotation << " ";
|
||||
|
||||
annotation << note->first << ":'" << note->second << "'";
|
||||
}
|
||||
line += annotation.str () + "] ";
|
||||
|
||||
// Description
|
||||
line += mDescription;
|
||||
|
||||
// EOL
|
||||
line += "\n";
|
||||
|
||||
if (line.length () > T_LINE_MAX)
|
||||
|
@ -421,10 +457,12 @@ void T::parse (const std::string& line)
|
|||
}
|
||||
else
|
||||
throw std::string ("Line too short");
|
||||
|
||||
mAnnotations.clear ();
|
||||
}
|
||||
break;
|
||||
|
||||
// File format version 2, from 2008.1.1
|
||||
// File format version 2, from 2008.1.1 - 2009.3.23
|
||||
case 2:
|
||||
{
|
||||
if (line.length () > 46) // ^.{36} . \[\] \[\] \n
|
||||
|
@ -473,6 +511,104 @@ void T::parse (const std::string& line)
|
|||
}
|
||||
else
|
||||
throw std::string ("Line too short");
|
||||
|
||||
mAnnotations.clear ();
|
||||
}
|
||||
break;
|
||||
|
||||
// File format version 3, from 2009.3.23
|
||||
case 3:
|
||||
{
|
||||
if (line.length () > 49) // ^.{36} . \[\] \[\] \[\] \n
|
||||
{
|
||||
mUUID = line.substr (0, 36);
|
||||
|
||||
mStatus = line[37] == '+' ? completed
|
||||
: line[37] == 'X' ? deleted
|
||||
: line[37] == 'r' ? recurring
|
||||
: pending;
|
||||
|
||||
size_t openTagBracket = line.find ("[");
|
||||
size_t closeTagBracket = line.find ("]", openTagBracket);
|
||||
if (openTagBracket != std::string::npos &&
|
||||
closeTagBracket != std::string::npos)
|
||||
{
|
||||
size_t openAttrBracket = line.find ("[", closeTagBracket);
|
||||
size_t closeAttrBracket = line.find ("]", openAttrBracket);
|
||||
if (openAttrBracket != std::string::npos &&
|
||||
closeAttrBracket != std::string::npos)
|
||||
{
|
||||
size_t openAnnoBracket = line.find ("[", closeAttrBracket);
|
||||
size_t closeAnnoBracket = line.find ("]", openAnnoBracket);
|
||||
if (openAnnoBracket != std::string::npos &&
|
||||
closeAnnoBracket != std::string::npos)
|
||||
{
|
||||
std::string tags = line.substr (
|
||||
openTagBracket + 1, closeTagBracket - openTagBracket - 1);
|
||||
std::vector <std::string> rawTags;
|
||||
split (mTags, tags, ' ');
|
||||
|
||||
std::string attributes = line.substr (
|
||||
openAttrBracket + 1, closeAttrBracket - openAttrBracket - 1);
|
||||
std::vector <std::string> pairs;
|
||||
split (pairs, attributes, ' ');
|
||||
for (size_t i = 0; i < pairs.size (); ++i)
|
||||
{
|
||||
std::vector <std::string> pair;
|
||||
split (pair, pairs[i], ':');
|
||||
if (pair.size () == 2)
|
||||
mAttributes[pair[0]] = pair[1];
|
||||
}
|
||||
|
||||
// Extract and split the annotations, which are of the form:
|
||||
// 1234:'...' 5678:'...'
|
||||
std::string annotations = line.substr (
|
||||
openAnnoBracket + 1, closeAnnoBracket - openAnnoBracket - 1);
|
||||
pairs.clear ();
|
||||
|
||||
std::string::size_type start = 0;
|
||||
std::string::size_type end = 0;
|
||||
do
|
||||
{
|
||||
end = annotations.find ('\'', start);
|
||||
if (end != std::string::npos)
|
||||
{
|
||||
end = annotations.find ('\'', end + 1);
|
||||
|
||||
if (start != std::string::npos &&
|
||||
end != std::string::npos)
|
||||
{
|
||||
pairs.push_back (annotations.substr (start, end - start + 1));
|
||||
start = end + 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
while (start != std::string::npos &&
|
||||
end != std::string::npos);
|
||||
|
||||
for (size_t i = 0; i < pairs.size (); ++i)
|
||||
{
|
||||
std::string pair = pairs[i];
|
||||
std::string::size_type colon = pair.find (":");
|
||||
if (colon != std::string::npos)
|
||||
{
|
||||
std::string name = pair.substr (0, colon);
|
||||
std::string value = pair.substr (colon + 2, pair.length () - colon - 3);
|
||||
mAnnotations[::atoi (name.c_str ())] = value;
|
||||
}
|
||||
}
|
||||
|
||||
mDescription = line.substr (closeAnnoBracket + 2, std::string::npos);
|
||||
}
|
||||
}
|
||||
else
|
||||
throw std::string ("Missing attribute brackets");
|
||||
}
|
||||
else
|
||||
throw std::string ("Missing tag brackets");
|
||||
}
|
||||
else
|
||||
throw std::string ("Line too short");
|
||||
}
|
||||
break;
|
||||
|
||||
|
@ -512,16 +648,31 @@ int T::determineVersion (const std::string& line)
|
|||
line[23] == '-' &&
|
||||
line[36] == ' ' &&
|
||||
(line[37] == '-' || line[37] == '+' || line[37] == 'X' || line[37] == 'r'))
|
||||
{
|
||||
// Version 3 looks like:
|
||||
//
|
||||
// uuid status [tags] [attributes] [annotations] description\n
|
||||
//
|
||||
// Scan for the number of [] pairs.
|
||||
std::string::size_type tagAtts = line.find ("] [", 0);
|
||||
std::string::size_type attsAnno = line.find ("] [", tagAtts + 1);
|
||||
std::string::size_type annoDesc = line.find ("] ", attsAnno + 1);
|
||||
if (tagAtts != std::string::npos &&
|
||||
attsAnno != std::string::npos &&
|
||||
annoDesc != std::string::npos)
|
||||
return 3;
|
||||
else
|
||||
return 2;
|
||||
}
|
||||
|
||||
// Version 3?
|
||||
// Version 4?
|
||||
//
|
||||
// Fortunately, with the hindsight that will come with version 3, the
|
||||
// identifying characteristics of 1 and 2 may be modified such that if 3 has
|
||||
// a UUID followed by a status, then there is still a way to differentiate
|
||||
// between 2 and 3.
|
||||
// Fortunately, with the hindsight that will come with version 4, the
|
||||
// identifying characteristics of 1, 2 and 3 may be modified such that if 4
|
||||
// has a UUID followed by a status, then there is still a way to differentiate
|
||||
// between 2, 3 and 4.
|
||||
//
|
||||
// The danger is that a version 2 binary reads and misinterprets a version 3
|
||||
// The danger is that a version 3 binary reads and misinterprets a version 4
|
||||
// file. This is why it is a good idea to rely on an explicit version
|
||||
// declaration rather than chance positioning.
|
||||
|
||||
|
|
6
src/T.h
6
src/T.h
|
@ -56,6 +56,7 @@ public:
|
|||
|
||||
const std::string getDescription () const { return mDescription; }
|
||||
void setDescription (const std::string& description) { mDescription = description; }
|
||||
int getAnnotationCount () const { return mAnnotations.size (); }
|
||||
|
||||
void getSubstitution (std::string&, std::string&) const;
|
||||
void setSubstitution (const std::string&, const std::string&);
|
||||
|
@ -77,6 +78,9 @@ public:
|
|||
void removeAttribute (const std::string&);
|
||||
void removeAttributes ();
|
||||
|
||||
void getAnnotations (std::map <time_t, std::string>&);
|
||||
void addAnnotation (const std::string&);
|
||||
|
||||
const std::string compose () const;
|
||||
const std::string composeCSV ();
|
||||
void parse (const std::string&);
|
||||
|
@ -93,9 +97,9 @@ private:
|
|||
std::vector<std::string> mTags;
|
||||
std::vector<std::string> mRemoveTags;
|
||||
std::map<std::string, std::string> mAttributes;
|
||||
|
||||
std::string mFrom;
|
||||
std::string mTo;
|
||||
std::map <time_t, std::string> mAnnotations;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
|
|
@ -227,7 +227,7 @@ void Table::setRowBg (const int row, const Text::color c)
|
|||
////////////////////////////////////////////////////////////////////////////////
|
||||
void Table::addCell (const int row, const int col, const std::string& data)
|
||||
{
|
||||
int length = 0;
|
||||
unsigned int length = 0;
|
||||
|
||||
if (mSuppressWS)
|
||||
{
|
||||
|
@ -238,7 +238,19 @@ void Table::addCell (const int row, const int col, const std::string& data)
|
|||
data2 = data;
|
||||
|
||||
clean (data2);
|
||||
// For multi-line cells, find the longest line.
|
||||
if (data2.find ("\n") != std::string::npos)
|
||||
{
|
||||
length = 0;
|
||||
std::vector <std::string> lines;
|
||||
split (lines, data2, "\n");
|
||||
for (unsigned int i = 0; i < lines.size (); ++i)
|
||||
if (lines[i].length () > length)
|
||||
length = lines[i].length ();
|
||||
}
|
||||
else
|
||||
length = data2.length ();
|
||||
|
||||
mData.add (row, col, data2);
|
||||
}
|
||||
else
|
||||
|
@ -248,11 +260,22 @@ void Table::addCell (const int row, const int col, const std::string& data)
|
|||
else
|
||||
mData.add (row, col, data);
|
||||
|
||||
// For multi-line cells, find the longest line.
|
||||
if (data.find ("\n") != std::string::npos)
|
||||
{
|
||||
length = 0;
|
||||
std::vector <std::string> lines;
|
||||
split (lines, data, "\n");
|
||||
for (unsigned int i = 0; i < lines.size (); ++i)
|
||||
if (lines[i].length () > length)
|
||||
length = lines[i].length ();
|
||||
}
|
||||
else
|
||||
length = data.length ();
|
||||
}
|
||||
|
||||
// Automatically maintain max width.
|
||||
mMaxDataWidth[col] = max (mMaxDataWidth[col], length);
|
||||
mMaxDataWidth[col] = max (mMaxDataWidth[col], (int)length);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -508,9 +531,7 @@ void Table::calculateColumnWidths ()
|
|||
}
|
||||
else
|
||||
{
|
||||
// std::cout << "# insufficient room, considering only flexible columns." << std::endl;
|
||||
|
||||
// The fallback position is to assume no width was specificed, and just
|
||||
// The fallback position is to assume no width was specified, and just
|
||||
// calculate widths accordingly.
|
||||
mTableWidth = 0;
|
||||
calculateColumnWidths ();
|
||||
|
|
|
@ -845,6 +845,14 @@ std::string handleAppend (TDB& tdb, T& task, Config& conf)
|
|||
{
|
||||
original.setId (task.getId ());
|
||||
tdb.modifyT (original);
|
||||
|
||||
if (conf.get ("echo.command", true))
|
||||
out << "Appended '"
|
||||
<< task.getDescription ()
|
||||
<< "' to task "
|
||||
<< task.getId ()
|
||||
<< std::endl;
|
||||
|
||||
}
|
||||
|
||||
return out.str ();
|
||||
|
@ -944,3 +952,34 @@ std::string handleColor (Config& conf)
|
|||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
std::string handleAnnotate (TDB& tdb, T& task, Config& conf)
|
||||
{
|
||||
std::stringstream out;
|
||||
std::vector <T> all;
|
||||
tdb.pendingT (all);
|
||||
|
||||
std::vector <T>::iterator it;
|
||||
for (it = all.begin (); it != all.end (); ++it)
|
||||
{
|
||||
if (it->getId () == task.getId ())
|
||||
{
|
||||
it->addAnnotation (task.getDescription ());
|
||||
tdb.modifyT (*it);
|
||||
|
||||
if (conf.get ("echo.command", true))
|
||||
out << "Annotated "
|
||||
<< task.getId ()
|
||||
<< " with '"
|
||||
<< task.getDescription ()
|
||||
<< "'"
|
||||
<< std::endl;
|
||||
|
||||
return out.str ();
|
||||
}
|
||||
}
|
||||
|
||||
throw std::string ("Task not found.");
|
||||
return out.str ();
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
|
|
@ -121,6 +121,7 @@ static const char* commands[] =
|
|||
"active",
|
||||
"add",
|
||||
"append",
|
||||
"annotate",
|
||||
"calendar",
|
||||
"colors",
|
||||
"completed",
|
||||
|
|
|
@ -207,7 +207,18 @@ std::string handleCompleted (TDB& tdb, T& task, Config& conf)
|
|||
|
||||
table.addCell (row, 0, end.toString (conf.get ("dateformat", "m/d/Y")));
|
||||
table.addCell (row, 1, refTask.getAttribute ("project"));
|
||||
table.addCell (row, 2, refTask.getDescription ());
|
||||
|
||||
std::string description = refTask.getDescription ();
|
||||
std::string when;
|
||||
std::map <time_t, std::string> annotations;
|
||||
refTask.getAnnotations (annotations);
|
||||
foreach (anno, annotations)
|
||||
{
|
||||
Date dt (anno->first);
|
||||
when = dt.toString (conf.get ("dateformat", "m/d/Y"));
|
||||
description += "\n" + when + " " + anno->second;
|
||||
}
|
||||
table.addCell (row, 2, description);
|
||||
|
||||
if (conf.get ("color", true) || conf.get (std::string ("_forcecolor"), false))
|
||||
{
|
||||
|
@ -270,7 +281,7 @@ std::string handleInfo (TDB& tdb, T& task, Config& conf)
|
|||
table.setTableDashedUnderline ();
|
||||
|
||||
table.setColumnWidth (0, Table::minimum);
|
||||
table.setColumnWidth (1, Table::minimum);
|
||||
table.setColumnWidth (1, Table::flexible);
|
||||
|
||||
table.setColumnJustification (0, Table::left);
|
||||
table.setColumnJustification (1, Table::left);
|
||||
|
@ -296,9 +307,20 @@ std::string handleInfo (TDB& tdb, T& task, Config& conf)
|
|||
: refTask.getStatus () == T::recurring ? "Recurring"
|
||||
: ""));
|
||||
|
||||
std::string description = refTask.getDescription ();
|
||||
std::string when;
|
||||
std::map <time_t, std::string> annotations;
|
||||
refTask.getAnnotations (annotations);
|
||||
foreach (anno, annotations)
|
||||
{
|
||||
Date dt (anno->first);
|
||||
when = dt.toString (conf.get ("dateformat", "m/d/Y"));
|
||||
description += "\n" + when + " " + anno->second;
|
||||
}
|
||||
|
||||
row = table.addRow ();
|
||||
table.addCell (row, 0, "Description");
|
||||
table.addCell (row, 1, refTask.getDescription ());
|
||||
table.addCell (row, 1, description);
|
||||
|
||||
if (refTask.getAttribute ("project") != "")
|
||||
{
|
||||
|
@ -1683,6 +1705,7 @@ std::string handleReportStats (TDB& tdb, T& task, Config& conf)
|
|||
int pendingT = 0;
|
||||
int completedT = 0;
|
||||
int taggedT = 0;
|
||||
int annotationsT = 0;
|
||||
int recurringT = 0;
|
||||
float daysPending = 0.0;
|
||||
int descLength = 0;
|
||||
|
@ -1713,6 +1736,8 @@ std::string handleReportStats (TDB& tdb, T& task, Config& conf)
|
|||
|
||||
descLength += it->getDescription ().length ();
|
||||
|
||||
annotationsT += it->getAnnotationCount ();
|
||||
|
||||
std::vector <std::string> tags;
|
||||
it->getTags (tags);
|
||||
if (tags.size ()) ++taggedT;
|
||||
|
@ -1760,6 +1785,7 @@ std::string handleReportStats (TDB& tdb, T& task, Config& conf)
|
|||
out << "Tasks tagged " << std::setprecision (3) << (100.0 * taggedT / totalT) << "%" << std::endl;
|
||||
}
|
||||
|
||||
out << "Annotations " << annotationsT << std::endl;
|
||||
out << "Unique tags " << allTags.size () << std::endl;
|
||||
out << "Projects " << allProjects.size () << std::endl;
|
||||
|
||||
|
@ -2164,7 +2190,7 @@ std::string handleCustomReport (
|
|||
}
|
||||
}
|
||||
|
||||
else if (*col == "description")
|
||||
else if (*col == "description_only")
|
||||
{
|
||||
table.addColumn ("Description");
|
||||
table.setColumnWidth (columnCount, Table::flexible);
|
||||
|
@ -2174,6 +2200,30 @@ std::string handleCustomReport (
|
|||
table.addCell (row, columnCount, tasks[row].getDescription ());
|
||||
}
|
||||
|
||||
else if (*col == "description")
|
||||
{
|
||||
table.addColumn ("Description");
|
||||
table.setColumnWidth (columnCount, Table::flexible);
|
||||
table.setColumnJustification (columnCount, Table::left);
|
||||
|
||||
std::string description;
|
||||
std::string when;
|
||||
for (unsigned int row = 0; row < tasks.size(); ++row)
|
||||
{
|
||||
description = tasks[row].getDescription ();
|
||||
std::map <time_t, std::string> annotations;
|
||||
tasks[row].getAnnotations (annotations);
|
||||
foreach (anno, annotations)
|
||||
{
|
||||
Date dt (anno->first);
|
||||
when = dt.toString (conf.get ("dateformat", "m/d/Y"));
|
||||
description += "\n" + when + " " + anno->second;
|
||||
}
|
||||
|
||||
table.addCell (row, columnCount, description);
|
||||
}
|
||||
}
|
||||
|
||||
else if (*col == "recur")
|
||||
{
|
||||
table.addColumn ("Recur");
|
||||
|
@ -2313,6 +2363,7 @@ void validReportColumns (const std::vector <std::string>& columns)
|
|||
*it != "active" &&
|
||||
*it != "tags" &&
|
||||
*it != "recur" &&
|
||||
*it != "description_only" &&
|
||||
*it != "description")
|
||||
bad.push_back (*it);
|
||||
|
||||
|
|
|
@ -88,6 +88,10 @@ static std::string shortUsage (Config& conf)
|
|||
table.addCell (row, 1, "task append [tags] [attrs] desc...");
|
||||
table.addCell (row, 2, "Appends more description to an existing task");
|
||||
|
||||
row = table.addRow ();
|
||||
table.addCell (row, 1, "task annotate ID desc...");
|
||||
table.addCell (row, 2, "Adds an annotation to an existing task");
|
||||
|
||||
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");
|
||||
|
@ -759,12 +763,12 @@ void updateShadowFile (TDB& tdb, Config& conf)
|
|||
|
||||
catch (std::string& error)
|
||||
{
|
||||
std::cout << error << std::endl;
|
||||
std::cerr << error << std::endl;
|
||||
}
|
||||
|
||||
catch (...)
|
||||
{
|
||||
std::cout << "Unknown error." << std::endl;
|
||||
std::cerr << "Unknown error." << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -833,6 +837,7 @@ std::string runTaskCommand (
|
|||
else if (command == "" && task.getId ()) { cmdMod = true; out = handleModify (tdb, task, conf); }
|
||||
else if (command == "add") { cmdMod = true; out = handleAdd (tdb, task, conf); }
|
||||
else if (command == "append") { cmdMod = true; out = handleAppend (tdb, task, conf); }
|
||||
else if (command == "annotate") { cmdMod = true; out = handleAnnotate (tdb, task, conf); }
|
||||
else if (command == "done") { cmdMod = true; out = handleDone (tdb, task, conf); }
|
||||
else if (command == "undelete") { cmdMod = true; out = handleUndelete (tdb, task, conf); }
|
||||
else if (command == "delete") { cmdMod = true; out = handleDelete (tdb, task, conf); }
|
||||
|
|
|
@ -88,6 +88,7 @@ std::string handleStart (TDB&, T&, Config&);
|
|||
std::string handleStop (TDB&, T&, Config&);
|
||||
std::string handleUndo (TDB&, T&, Config&);
|
||||
std::string handleColor (Config&);
|
||||
std::string handleAnnotate (TDB&, T&, Config&);
|
||||
|
||||
// report.cpp
|
||||
void filter (std::vector<T>&, T&);
|
||||
|
|
74
src/tests/annotate.t
Executable file
74
src/tests/annotate.t
Executable file
|
@ -0,0 +1,74 @@
|
|||
#! /usr/bin/perl
|
||||
################################################################################
|
||||
## task - a command line task list manager.
|
||||
##
|
||||
## Copyright 2006 - 2009, Paul Beckingham.
|
||||
## All rights reserved.
|
||||
##
|
||||
## This program is free software; you can redistribute it and/or modify it under
|
||||
## the terms of the GNU General Public License as published by the Free Software
|
||||
## Foundation; either version 2 of the License, or (at your option) any later
|
||||
## version.
|
||||
##
|
||||
## This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
## FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
## details.
|
||||
##
|
||||
## You should have received a copy of the GNU General Public License along with
|
||||
## this program; if not, write to the
|
||||
##
|
||||
## Free Software Foundation, Inc.,
|
||||
## 51 Franklin Street, Fifth Floor,
|
||||
## Boston, MA
|
||||
## 02110-1301
|
||||
## USA
|
||||
##
|
||||
################################################################################
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
use Test::More tests => 8;
|
||||
|
||||
# Create the rc file.
|
||||
if (open my $fh, '>', 'annotate.rc')
|
||||
{
|
||||
print $fh "data.location=.\n",
|
||||
"report.r.description=r\n",
|
||||
"report.r.columns=id,description\n",
|
||||
"report.r.sort=id+\n";
|
||||
close $fh;
|
||||
ok (-r 'annotate.rc', 'Created annotate.rc');
|
||||
}
|
||||
|
||||
# Add two tasks, annotate one twice.
|
||||
qx{../task rc:annotate.rc add one};
|
||||
qx{../task rc:annotate.rc add two};
|
||||
qx{../task rc:annotate.rc annotate 1 foo};
|
||||
sleep 2;
|
||||
qx{../task rc:annotate.rc annotate 1 bar};
|
||||
my $output = qx{../task rc:annotate.rc r};
|
||||
|
||||
# ID Description
|
||||
# -- -------------------------------
|
||||
# 1 one
|
||||
# 3/24/2009 foo
|
||||
# 3/24/2009 bar
|
||||
# 2 two
|
||||
#
|
||||
# 2 tasks
|
||||
like ($output, qr/1 one/, 'task 1');
|
||||
like ($output, qr/2 two/, 'task 2');
|
||||
like ($output, qr/one.+\d{1,2}\/\d{1,2}\/\d{4} foo/ms, 'first annotation');
|
||||
like ($output, qr/foo.+\d{1,2}\/\d{1,2}\/\d{4} bar/ms, 'second annotation');
|
||||
like ($output, qr/2 tasks/, 'count');
|
||||
|
||||
# Cleanup.
|
||||
unlink 'pending.data';
|
||||
ok (!-r 'pending.data', 'Removed pending.data');
|
||||
|
||||
unlink 'annotate.rc';
|
||||
ok (!-r 'annotate.rc', 'Removed annotate.rc');
|
||||
|
||||
exit 0;
|
||||
|
|
@ -31,11 +31,11 @@
|
|||
////////////////////////////////////////////////////////////////////////////////
|
||||
int main (int argc, char** argv)
|
||||
{
|
||||
UnitTest test (5);
|
||||
UnitTest test (8);
|
||||
|
||||
T t;
|
||||
std::string s = t.compose ();
|
||||
test.is ((int)s.length (), 46, "T::T (); T::compose ()");
|
||||
test.is ((int)s.length (), 49, "T::T (); T::compose ()");
|
||||
test.diag (s);
|
||||
|
||||
t.setStatus (T::completed);
|
||||
|
@ -54,11 +54,26 @@ int main (int argc, char** argv)
|
|||
test.diag (s);
|
||||
|
||||
// Round trip test.
|
||||
std::string sample = "00000000-0000-0000-0000-000000000000 - [] [] Sample";
|
||||
std::string sample = "00000000-0000-0000-0000-000000000000 - [] [] [] Sample";
|
||||
T t2;
|
||||
t2.parse (sample);
|
||||
sample += "\n";
|
||||
test.is (t2.compose (), sample, "T::parse -> T::compose round trip");
|
||||
|
||||
// b10b3236-70d8-47bb-840a-b4c430758fb6 - [foo] [bar:baz] [1237865996:'woof'] sample\n
|
||||
// ....:....|....:....|....:....|....:....|....:....|....:....|....:....|....:....|....:....|
|
||||
// ^ ^ ^
|
||||
// 0 36 66
|
||||
t.setStatus (T::pending);
|
||||
t.addTag ("foo");
|
||||
t.setAttribute ("bar", "baz");
|
||||
t.addAnnotation ("woof");
|
||||
t.setDescription ("sample");
|
||||
std::string format = t.compose ();
|
||||
test.is (format.substr (36, 20), " - [foo] [bar:baz] [", "compose tag, attribute");
|
||||
test.is (format.substr (66, 16), ":'woof'] sample\n", "compose annotation");
|
||||
test.is (t.getAnnotationCount (), 1, "annotation count");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue