diff --git a/ChangeLog b/ChangeLog index f48ea6a38..295c0fcc1 100644 --- a/ChangeLog +++ b/ChangeLog @@ -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 ------------------------------ diff --git a/html/advanced.html b/html/advanced.html index f0863e6f6..fed284b47 100644 --- a/html/advanced.html +++ b/html/advanced.html @@ -102,6 +102,25 @@ Car 2 2 wks 25% XXXXXXXXX Appends the additional description to an existing task.

+ % task annotate <id> additional note... +

+ 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: +

+ +
% 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
+

+ The date of the annotation uses the "dateformat" configuration + variable. +

+ % task delete <id>

There are two ways of getting rid of tasks - mark them as done, or diff --git a/html/custom.html b/html/custom.html index 1ee499a35..67f51b9e0 100644 --- a/html/custom.html +++ b/html/custom.html @@ -100,6 +100,7 @@ report.mine.sort=priority-,project+

  • active
  • tags
  • recur +
  • description_only
  • description diff --git a/html/task.html b/html/task.html index cb2e736e2..787655ecb 100644 --- a/html/task.html +++ b/html/task.html @@ -108,6 +108,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.

    diff --git a/html/usage.html b/html/usage.html index f8ab9e814..51979c632 100644 --- a/html/usage.html +++ b/html/usage.html @@ -37,6 +37,7 @@

    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/
    diff --git a/src/Config.cpp b/src/Config.cpp
    index de73f42dc..eef4e5899 100644
    --- a/src/Config.cpp
    +++ b/src/Config.cpp
    @@ -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");
    diff --git a/src/T.cpp b/src/T.cpp
    index f490d8681..b7f038c35 100644
    --- a/src/T.cpp
    +++ b/src/T.cpp
    @@ -25,6 +25,7 @@
     //
     ////////////////////////////////////////////////////////////////////////////////
     #include 
    +#include 
     #include 
     #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 & 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
    @@ -446,24 +484,122 @@ void T::parse (const std::string& line)
               if (openAttrBracket  != std::string::npos &&
                   closeAttrBracket != std::string::npos)
               {
    -             std::string tags = line.substr (
    -               openTagBracket + 1, closeTagBracket - openTagBracket - 1);
    -             std::vector  rawTags;
    -             split (mTags, tags, ' ');
    +            std::string tags = line.substr (
    +              openTagBracket + 1, closeTagBracket - openTagBracket - 1);
    +            std::vector  rawTags;
    +            split (mTags, tags, ' ');
     
    -             std::string attributes = line.substr (
    -               openAttrBracket + 1, closeAttrBracket - openAttrBracket - 1);
    -             std::vector  pairs;
    -             split (pairs, attributes, ' ');
    -             for (size_t i = 0; i <  pairs.size (); ++i)
    -             {
    -               std::vector  pair;
    -               split (pair, pairs[i], ':');
    -               if (pair.size () == 2)
    -                 mAttributes[pair[0]] = pair[1];
    -             }
    +            std::string attributes = line.substr (
    +              openAttrBracket + 1, closeAttrBracket - openAttrBracket - 1);
    +            std::vector  pairs;
    +            split (pairs, attributes, ' ');
    +            for (size_t i = 0; i <  pairs.size (); ++i)
    +            {
    +              std::vector  pair;
    +              split (pair, pairs[i], ':');
    +              if (pair.size () == 2)
    +                mAttributes[pair[0]] = pair[1];
    +            }
     
    -             mDescription = line.substr (closeAttrBracket + 2, std::string::npos);
    +            mDescription = line.substr (closeAttrBracket + 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");
    +
    +      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  rawTags;
    +              split (mTags, tags, ' ');
    +
    +              std::string attributes = line.substr (
    +                openAttrBracket + 1, closeAttrBracket - openAttrBracket - 1);
    +              std::vector  pairs;
    +              split (pairs, attributes, ' ');
    +              for (size_t i = 0; i <  pairs.size (); ++i)
    +              {
    +                std::vector  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");
    @@ -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'))
    -    return 2;
    +  {
    +    // 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.
     
    diff --git a/src/T.h b/src/T.h
    index fc126ae34..7f3898e2b 100644
    --- a/src/T.h
    +++ b/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 &);
    +  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           mTags;
       std::vector           mRemoveTags;
       std::map mAttributes;
    -
       std::string                        mFrom;
       std::string                        mTo;
    +  std::map      mAnnotations;
     };
     
     #endif
    diff --git a/src/Table.cpp b/src/Table.cpp
    index 6f20c1b50..0ea6a57cb 100644
    --- a/src/Table.cpp
    +++ b/src/Table.cpp
    @@ -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);
    -    length = data2.length ();
    +    // For multi-line cells, find the longest line.
    +    if (data2.find ("\n") != std::string::npos)
    +    {
    +      length = 0;
    +      std::vector  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);
     
    -    length = data.length ();
    +    // For multi-line cells, find the longest line.
    +    if (data.find ("\n") != std::string::npos)
    +    {
    +      length = 0;
    +      std::vector  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 ();
    diff --git a/src/command.cpp b/src/command.cpp
    index b4f039803..e356ae6d8 100644
    --- a/src/command.cpp
    +++ b/src/command.cpp
    @@ -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  all;
    +  tdb.pendingT (all);
    +
    +  std::vector ::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 ();
    +}
    +
    +////////////////////////////////////////////////////////////////////////////////
    diff --git a/src/parse.cpp b/src/parse.cpp
    index bcda9e778..56b32a98a 100644
    --- a/src/parse.cpp
    +++ b/src/parse.cpp
    @@ -121,6 +121,7 @@ static const char* commands[] =
       "active",
       "add",
       "append",
    +  "annotate",
       "calendar",
       "colors",
       "completed",
    diff --git a/src/report.cpp b/src/report.cpp
    index 44752327a..76c5c75f4 100644
    --- a/src/report.cpp
    +++ b/src/report.cpp
    @@ -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  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  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  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  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");
    @@ -2302,17 +2352,18 @@ void validReportColumns (const std::vector & columns)
     
       std::vector ::const_iterator it;
       for (it = columns.begin (); it != columns.end (); ++it)
    -    if (*it != "id"       &&
    -        *it != "uuid"     &&
    -        *it != "project"  &&
    -        *it != "priority" &&
    -        *it != "entry"    &&
    -        *it != "start"    &&
    -        *it != "due"      &&
    -        *it != "age"      &&
    -        *it != "active"   &&
    -        *it != "tags"     &&
    -        *it != "recur"    &&
    +    if (*it != "id"               &&
    +        *it != "uuid"             &&
    +        *it != "project"          &&
    +        *it != "priority"         &&
    +        *it != "entry"            &&
    +        *it != "start"            &&
    +        *it != "due"              &&
    +        *it != "age"              &&
    +        *it != "active"           &&
    +        *it != "tags"             &&
    +        *it != "recur"            &&
    +        *it != "description_only" &&
             *it != "description")
           bad.push_back (*it);
     
    diff --git a/src/task.cpp b/src/task.cpp
    index 70d2f550c..ac3ed392b 100644
    --- a/src/task.cpp
    +++ b/src/task.cpp
    @@ -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); }
    diff --git a/src/task.h b/src/task.h
    index 5dacbd4c3..ab25f2d14 100644
    --- a/src/task.h
    +++ b/src/task.h
    @@ -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&);
    diff --git a/src/tests/annotate.t b/src/tests/annotate.t
    new file mode 100755
    index 000000000..bd867f8d4
    --- /dev/null
    +++ b/src/tests/annotate.t
    @@ -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;
    +
    diff --git a/src/tests/t.t.cpp b/src/tests/t.t.cpp
    index ae76b18c6..d98fca274 100644
    --- a/src/tests/t.t.cpp
    +++ b/src/tests/t.t.cpp
    @@ -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;
     }