diff --git a/ChangeLog b/ChangeLog index 584e4e922..4656eec5e 100644 --- a/ChangeLog +++ b/ChangeLog @@ -4,6 +4,14 @@ 1.7.0 (?) ? + Improved the errors when parsing a corrupt or unrecognized pending.data or completed.data file (thanks to T. Charles Yun). + + Added details to the "info" report about recurring tasks (thanks to T. + Charles Yun). + + Now writes a sample "defaultwidth" configuration variable to the default + .taskrc file (thanks to T. Charles Yun). + + Task allows commands that require an ID to now be given a sequence, which + is a set of IDs. This allows commands like "task delete 1 2 5-10,12". + + Fixed bug in the ghistory report, which caused it to only show a new + month if a task was added during that month. ------ old releases ------------------------------ diff --git a/README b/README index 01b01accd..5427555d0 100644 --- a/README +++ b/README @@ -44,7 +44,11 @@ All feedback is welcome, in addition to any bug reports or patches to: task@beckingham.net -Got an idea for an enhancement? Send a message! +Or better yet, get involved in the discussion at + + http://groups.google.com/group/taskprogram + +Got an idea for an enhancement? Post a message! I have found that task makes me more productive and organized. I hope task can do the same for you. diff --git a/checklist.txt b/checklist.txt new file mode 100644 index 000000000..5cec663c1 --- /dev/null +++ b/checklist.txt @@ -0,0 +1,26 @@ +Release Checklist +----------------- + +- Update "Upcoming Features" document on group +- Ensure all unit tests pass on OS X +- Ensure clean build on OS X +- Make a source package (1) +- Ensure clean build on latest Fedora Core from source package +- Git clone and rebuild, ensure all unit tests pass +- Ensure clean build on latest Ubuntu from source package +- Git clone and rebuild, ensure all unit tests pass +- Ensure clean build on Windows/Cygwin from source package +- Git clone and rebuild, ensure all unit tests pass +- Make a new source package (2) +- Add actual release date to ChangeLog +- Add actual release date to html/task.html +- Merge version branch to master +- Tag master +- Make a new source package (3) +- Send source package to package maintainer +- Make OS X .pkg package +- Wait for all packages +- Upload all packages to website +- Upload all docs to website +- Send announcement to group + diff --git a/html/30second.html b/html/30second.html index 4a3ac3c2d..38cef96ff 100644 --- a/html/30second.html +++ b/html/30second.html @@ -20,7 +20,6 @@ Shell Configuration Colors - Usage Recurrence Date Handling FAQ diff --git a/html/advanced.html b/html/advanced.html index d5afcba85..f157fb68a 100644 --- a/html/advanced.html +++ b/html/advanced.html @@ -20,7 +20,6 @@ Shell Configuration Colors - Usage Recurrence Date Handling FAQ diff --git a/html/color.html b/html/color.html index ecc060cb8..37e985f0d 100644 --- a/html/color.html +++ b/html/color.html @@ -20,7 +20,6 @@ Shell Configuration Colors - Usage Recurrence Date Handling FAQ diff --git a/html/config.html b/html/config.html index db38ba1b1..96b0c4a15 100644 --- a/html/config.html +++ b/html/config.html @@ -20,7 +20,6 @@ Shell Configuration Colors - Usage Recurrence Date Handling FAQ diff --git a/html/custom.html b/html/custom.html index 02ae73e5b..fa03a5249 100644 --- a/html/custom.html +++ b/html/custom.html @@ -20,7 +20,6 @@ Shell Configuration Colors - Usage Recurrence Date Handling FAQ diff --git a/html/date.html b/html/date.html index 68ff201a0..d113993f7 100644 --- a/html/date.html +++ b/html/date.html @@ -20,7 +20,6 @@ Shell Configuration Colors - Usage Recurrence Date Handling FAQ diff --git a/html/faq.html b/html/faq.html index 289c58d10..73c47fc2c 100644 --- a/html/faq.html +++ b/html/faq.html @@ -20,7 +20,6 @@ Shell Configuration Colors - Usage Recurrence Date Handling FAQ diff --git a/html/filter.html b/html/filter.html index a561bdd58..373be27f5 100644 --- a/html/filter.html +++ b/html/filter.html @@ -20,7 +20,6 @@ Shell Configuration Colors - Usage Recurrence Date Handling FAQ diff --git a/html/import.html b/html/import.html index de227c2a7..3a488af36 100644 --- a/html/import.html +++ b/html/import.html @@ -20,7 +20,6 @@ Shell Configuration Colors - Usage Recurrence Date Handling FAQ diff --git a/html/links.html b/html/links.html index 15642d0b2..609f023a4 100644 --- a/html/links.html +++ b/html/links.html @@ -20,7 +20,6 @@ Shell Configuration Colors - Usage Recurrence Date Handling FAQ diff --git a/html/recur.html b/html/recur.html index e4365fcec..a6963b842 100644 --- a/html/recur.html +++ b/html/recur.html @@ -20,7 +20,6 @@ Shell Configuration Colors - Usage Recurrence Date Handling FAQ @@ -52,7 +51,7 @@ generated on a regular basis. Consider the example:

-
% task Pay rent due:7/1/2008 recur:monthly
+
% task add Pay rent due:7/1/2008 recur:monthly

If today's date is 7/10, for example, then that due date is in the past, and @@ -81,7 +80,7 @@ ID Project Pri Due Active Age Description Thursdays instead:

-
% task TPS report due:thursday recur:weekly until:8/31/2008
+
% task add TPS report due:thursday recur:weekly until:8/31/2008

This create a weekly recurring task that expires on 8/31/2008. What this means diff --git a/html/sequence.html b/html/sequence.html new file mode 100644 index 000000000..aa556ac3e --- /dev/null +++ b/html/sequence.html @@ -0,0 +1,154 @@ + + + + Task Usage + + + + + +

+ + + + + + +
+ + +
+
+
+
+

ID Sequences

+
+

+ Some task commands require an ID to be specified. For example: +

+ +
% task 3 done
+ +

+ This marks a single task as done. But if you wanted to mark + several tasks as done, you could use: +

+ +
% task 3,4,5 done
+ +

+ Which would mark tasks 3, 4 and 5 as all done. In this example, + the three IDs are consecutive, which means you could also have + entered: +

+ +
% task 3-5 done
+ +

+ Or in a more complex example: +

+ +
% task 1,3-5,12 23-25 done
+ +

+ This would mark tasks 1, 3, 4, 5, 12, 23, 24 and 25 as done. + Note that this example uses two sequences, separated by a space. +

+ +

+ You must be careful though. Task tries very carefully to do + the right thing when it interprets the command line, but must + still impose some rules so that it can unambiguously read the + command. If you use one or more sequences, then they must + appear on the command line adjacent to each other. If they + are separated by something else, then task assumes the second + and subsequent set is not a sequence. Here is an example + of this: +

+ +
% task 3 Order part number 4-123
+ +

+ Clearly the 4-123 is a part number, and not a sequence. + Task is being asked to modify the description of task 3 to be + "Order part number 4-123". Note that the ID is separated + from the part number by something other than a sequence. + Here is a bad example that task will misinterpret: +

+ +
% task 3 4-123 is back-ordered, try again next week
+ +

+ The intent here is that task 3 have its description modified to be + "40123 is back-ordered, try again next week", but will be + misinterpreted as tasks 3, 4, 5, 6 ... 123 will all be modified + to have the description "is back-ordered, try again next week". + The solution is to quote the whole description: +

+ +
% task 3 "4-123 is back-ordered, try again next week"
+
+ +
+
+
+

+ Copyright 2006-2009, P. Beckingham. All rights reserved. +

+
+ +
+
+
+
+
+
+
+
+
+
+
+
+ + + +
+ +
+ + + + + + + diff --git a/html/setup.html b/html/setup.html index bf8109565..2cbcad404 100644 --- a/html/setup.html +++ b/html/setup.html @@ -20,7 +20,6 @@ Shell Configuration Colors - Usage Recurrence Date Handling FAQ diff --git a/html/shadow.html b/html/shadow.html index 63285866b..1488a0167 100644 --- a/html/shadow.html +++ b/html/shadow.html @@ -20,7 +20,6 @@ Shell Configuration Colors - Usage Recurrence Date Handling FAQ diff --git a/html/shell.html b/html/shell.html index d868b5bec..661f3723c 100644 --- a/html/shell.html +++ b/html/shell.html @@ -20,7 +20,6 @@ Shell Configuration Colors - Usage Recurrence Date Handling FAQ diff --git a/html/simple.html b/html/simple.html index 1b201cbb5..c256422fd 100644 --- a/html/simple.html +++ b/html/simple.html @@ -20,7 +20,6 @@ Shell Configuration Colors - Usage Recurrence Date Handling FAQ diff --git a/html/task.html b/html/task.html index b46178eab..ac3e2038e 100644 --- a/html/task.html +++ b/html/task.html @@ -20,7 +20,6 @@ Shell Configuration Colors - Usage Recurrence Date Handling FAQ @@ -50,7 +49,6 @@
  • Interacting with the Shell
  • Configuring Task
  • Color -
  • Task Command Usage
  • Recurring Tasks
  • Date Handling
  • Old Versions @@ -59,6 +57,7 @@
  • Custom Reports
  • Data Import
  • Frequently Asked Questions +
  • ID Sequences

    @@ -71,6 +70,12 @@ which illustrates many of task's features.

    +

    + For the latest news, discussion of proposed task features, and + somewhere to voice your opinions, join us at + http://groups.google.com/group/taskprogram. +

    +

    Get the Latest Stable Release

    @@ -110,6 +115,14 @@

    diff --git a/html/usage.html b/html/usage.html deleted file mode 100644 index 419e04b0c..000000000 --- a/html/usage.html +++ /dev/null @@ -1,153 +0,0 @@ - - - - Task Usage - - - - - -

    - - - - - - -
    - - -
    -
    -
    -
    -

    Command Usage

    -
    -
    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/
    -       task delete ID
    -       task undelete ID
    -       task info ID
    -       task start ID
    -       task stop ID
    -       task done ID
    -       task undo ID
    -       task projects
    -       task tags
    -       task summary
    -       task history
    -       task ghistory
    -       task next
    -       task calendar
    -       task active
    -       task overdue
    -       task stats
    -       task export
    -       task color
    -       task version
    -       task help
    -       task list [tags] [attrs] desc...
    -       task long [tags] [attrs] desc...
    -       task ls [tags] [attrs] desc...
    -       task newest [tags] [attrs] desc...
    -       task oldest [tags] [attrs] desc...
    -
    -See http://www.beckingham.net/task.html for the latest releases and a full tutorial.
    -
    -ID is the numeric identifier displayed by the 'task list' command
    -
    -Tags are arbitrary words, any quantity:
    -  +tag               The + means add the tag
    -  -tag               The - means remove the tag
    -
    -Attributes are:
    -  project:           Project name
    -  priority:          Priority
    -  due:               Due date
    -  recur:             Recurrence frequency
    -  until:             Recurrence end date
    -  fg:                Foreground color
    -  bg:                Background color
    -  rc:                Alternate .taskrc file
    -
    -Any command or attribute name may be abbreviated if still unique:
    -  task list project:Home
    -  task li       pro:Home
    -
    -Some task descriptions need to be escaped because of the shell:
    -  task add "quoted ' quote"
    -  task add escaped \' quote
    -
    -Many characters have special meaning to the shell, including:
    -  $ ! ' " ( ) ; \ ` * ? { } [ ] < > | & % # ~
    -
    - -
    -
    -
    -

    - Copyright 2006-2009, P. Beckingham. All rights reserved. -

    -
    - -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    - - - -
    - -
    - - - - - - - diff --git a/html/versions.html b/html/versions.html index 0ec6168ab..957f38a44 100644 --- a/html/versions.html +++ b/html/versions.html @@ -20,7 +20,6 @@ Shell Configuration Colors - Usage Recurrence Date Handling FAQ diff --git a/src/Config.cpp b/src/Config.cpp index 5403accec..1b07606a8 100644 --- a/src/Config.cpp +++ b/src/Config.cpp @@ -149,6 +149,7 @@ void Config::createDefault (const std::string& home) fprintf (out, "next=2\n"); fprintf (out, "dateformat=m/d/Y\n"); fprintf (out, "#monthsperline=2\n"); + fprintf (out, "#defaultwidth=80\n"); fprintf (out, "curses=on\n"); fprintf (out, "color=on\n"); fprintf (out, "due=7\n"); diff --git a/src/T.cpp b/src/T.cpp index 842613c56..58a280f62 100644 --- a/src/T.cpp +++ b/src/T.cpp @@ -37,6 +37,7 @@ T::T () mUUID = uuid (); mStatus = pending; mId = 0; + mSequence.clear (); mTags.clear (); mAttributes.clear (); mDescription = ""; @@ -59,6 +60,7 @@ T::T (const T& other) mStatus = other.mStatus; mUUID = other.mUUID; mId = other.mId; + mSequence = other.mSequence; mDescription = other.mDescription; mTags = other.mTags; mRemoveTags = other.mRemoveTags; @@ -74,6 +76,7 @@ T& T::operator= (const T& other) mStatus = other.mStatus; mUUID = other.mUUID; mId = other.mId; + mSequence = other.mSequence; mDescription = other.mDescription; mTags = other.mTags; mRemoveTags = other.mRemoveTags; @@ -286,6 +289,16 @@ void T::addAnnotation (const std::string& description) mAnnotations[time (NULL)] = sanitized; } +//////////////////////////////////////////////////////////////////////////////// +bool T::sequenceContains (int id) const +{ + foreach (seq, mSequence) + if (*seq == id) + return true; + + return false; +} + //////////////////////////////////////////////////////////////////////////////// // uuid status [tags] [attributes] [annotations] description // @@ -575,8 +588,6 @@ void T::parse (const std::string& line) openAttrBracket + 1, closeAttrBracket - openAttrBracket - 1); std::vector pairs; split (pairs, attributes, ' '); - if (pairs.size () == 0) - throw std::string ("Could not find any attributes."); for (size_t i = 0; i < pairs.size (); ++i) { diff --git a/src/T.h b/src/T.h index 14004a9c3..fac345efa 100644 --- a/src/T.h +++ b/src/T.h @@ -49,7 +49,9 @@ public: void setUUID (const std::string& uuid) { mUUID = uuid; } int getId () const { return mId; } - void setId (int id) { mId = id; } + void setId (int id) { mId = id; mSequence.push_back (id); } + std::vector getAllIds () const { return mSequence; } + void addId (int id) { if (mId == 0) mId = id; mSequence.push_back (id); } status getStatus () const { return mStatus; } void setStatus (status s) { mStatus = s; } @@ -82,6 +84,7 @@ public: void getAnnotations (std::map &) const; void setAnnotations (const std::map &); void addAnnotation (const std::string&); + bool sequenceContains (int) const; const std::string compose () const; const std::string composeCSV (); @@ -95,6 +98,7 @@ private: status mStatus; std::string mUUID; int mId; + std::vector mSequence; std::string mDescription; std::vector mTags; std::vector mRemoveTags; diff --git a/src/command.cpp b/src/command.cpp index 968763656..fcc512fb9 100644 --- a/src/command.cpp +++ b/src/command.cpp @@ -193,42 +193,34 @@ std::string handleTags (TDB& tdb, T& task, Config& conf) std::string handleUndelete (TDB& tdb, T& task, Config& conf) { std::stringstream out; + std::vector all; tdb.allPendingT (all); + filterSequence (all, task); - int id = task.getId (); - std::vector ::iterator it; - for (it = all.begin (); it != all.end (); ++it) + foreach (t, all) { - if (it->getId () == id) + if (t->getStatus () == T::deleted) { - if (it->getStatus () == T::deleted) - { - if (it->getAttribute ("recur") != "") - { - out << "Task does not support 'undelete' for recurring tasks." << std::endl; - return out.str (); - } + if (t->getAttribute ("recur") != "") + out << "Task does not support 'undo' for recurring tasks.\n"; - T restored (*it); - restored.setStatus (T::pending); - restored.removeAttribute ("end"); - tdb.modifyT (restored); + t->setStatus (T::pending); + t->removeAttribute ("end"); + tdb.modifyT (*t); - out << "Task " << id << " successfully undeleted." << std::endl; - return out.str (); - } - else - { - out << "Task " << id << " is not deleted - therefore cannot undelete." << std::endl; - return out.str (); - } + out << "Task " << t->getId () << " '" << t->getDescription () << "' successfully undeleted.\n"; + } + else + { + out << "Task " << t->getId () << " '" << t->getDescription () << "' is not deleted - therefore cannot be undeleted.\n"; } } - out << "Task " << id - << " not found - tasks can only be reliably undeleted if the undelete" << std::endl - << "command is run immediately after the errant delete command." << std::endl; + out << "\n" + << "Please note that tasks can only be reliably undeleted if the undelete " + << "command is run immediately after the errant delete command." + << std::endl; return out.str (); } @@ -242,37 +234,31 @@ std::string handleUndo (TDB& tdb, T& task, Config& conf) std::vector all; tdb.allPendingT (all); + filterSequence (all, task); - int id = task.getId (); - std::vector ::iterator it; - for (it = all.begin (); it != all.end (); ++it) + foreach (t, all) { - if (it->getId () == id) + if (t->getStatus () == T::completed) { - if (it->getStatus () == T::completed) - { - if (it->getAttribute ("recur") != "") - return std::string ("Task does not support 'undo' for recurring tasks.\n"); + if (t->getAttribute ("recur") != "") + out << "Task does not support 'undo' for recurring tasks.\n"; - T restored (*it); - restored.setStatus (T::pending); - restored.removeAttribute ("end"); - tdb.modifyT (restored); + t->setStatus (T::pending); + t->removeAttribute ("end"); + tdb.modifyT (*t); - out << "Task " << id << " successfully undone." << std::endl; - return out.str (); - } - else - { - out << "Task " << id << " is not done - therefore cannot be undone." << std::endl; - return out.str (); - } + out << "Task " << t->getId () << " '" << t->getDescription () << "' successfully undone." << std::endl; + } + else + { + out << "Task " << t->getId () << " '" << t->getDescription () << "' is not done - therefore cannot be undone." << std::endl; } } - out << "Task " << id - << " not found - tasks can only be reliably undone if the undo" << std::endl - << "command is run immediately after the errant done command." << std::endl; + out << std::endl + << "Please note that tasks can only be reliably undone if the undo " + << "command is run immediately after the errant done command." + << std::endl; return out.str (); } @@ -367,8 +353,9 @@ std::string handleVersion (Config& conf) // Complain about configuration variables that are not recognized. // These are the regular configuration variables. + // Note that there is a leading and trailing space. std::string recognized = - "blanklines color color.active color.due color.overdue color.pri.H " + " blanklines color color.active color.due color.overdue color.pri.H " "color.pri.L color.pri.M color.pri.none color.recurring color.tagged " "confirmation curses data.location dateformat default.command " "default.priority defaultwidth due echo.command locking monthsperline nag " @@ -377,7 +364,7 @@ std::string handleVersion (Config& conf) "import.synonym.tags import.synonym.entry import.synonym.start " "import.synonym.due import.synonym.recur import.synonym.end " "import.synonym.project import.synonym.priority import.synonym.fg " - "import.synonym.bg import.synonym.description"; + "import.synonym.bg import.synonym.description "; // This configuration variable is supported, but not documented. It exists // so that unit tests can force color to be on even when the output from task @@ -387,7 +374,10 @@ std::string handleVersion (Config& conf) std::vector unrecognized; foreach (i, all) { - if (recognized.find (*i) == std::string::npos) + // Disallow partial matches by tacking a leading an trailing space on each + // variable name. + std::string pattern = " " + *i + " "; + if (recognized.find (pattern) == std::string::npos) { // These are special configuration variables, because their name is // dynamic. @@ -439,68 +429,75 @@ std::string handleDelete (TDB& tdb, T& task, Config& conf) { std::stringstream out; - if (!conf.get (std::string ("confirmation"), false) || confirm ("Permanently delete task?")) + std::vector all; + tdb.allPendingT (all); + filterSequence (all, task); + + foreach (t, all) { - std::vector all; - tdb.allPendingT (all); - foreach (t, all) + std::stringstream question; + question << "Permanently delete task " + << t->getId () + << " '" + << t->getDescription () + << "'?"; + + if (!conf.get (std::string ("confirmation"), false) || confirm (question.str ())) { - if (t->getId () == task.getId ()) + // Check for the more complex case of a recurring task. If this is a + // recurring task, get confirmation to delete them all. + std::string parent = t->getAttribute ("parent"); + if (parent != "") { - // Check for the more complex case of a recurring task. If this is a - // recurring task, get confirmation to delete them all. - std::string parent = t->getAttribute ("parent"); - if (parent != "") + if (confirm ("This is a recurring task. Do you want to delete all pending recurrences of this same task?")) { - if (confirm ("This is a recurring task. Do you want to delete all pending recurrences of this same task?")) + // Scan all pending tasks for siblings of this task, and the parent + // itself, and delete them. + foreach (sibling, all) { - // Scan all pending tasks for siblings of this task, and the parent - // itself, and delete them. - foreach (sibling, all) + if (sibling->getAttribute ("parent") == parent || + sibling->getUUID () == parent) { - if (sibling->getAttribute ("parent") == parent || - sibling->getUUID () == parent) - { - tdb.deleteT (*sibling); - if (conf.get ("echo.command", true)) - out << "Deleting recurring task " - << sibling->getId () - << " " - << sibling->getDescription () - << std::endl; - } + tdb.deleteT (*sibling); + if (conf.get ("echo.command", true)) + out << "Deleting recurring task " + << sibling->getId () + << " '" + << sibling->getDescription () + << "'" + << std::endl; } } - else - { - // Update mask in parent. - t->setStatus (T::deleted); - updateRecurrenceMask (tdb, all, *t); - tdb.deleteT (*t); - out << "Deleting recurring task " - << t->getId () - << " " - << t->getDescription () - << std::endl; - } } else { + // Update mask in parent. + t->setStatus (T::deleted); + updateRecurrenceMask (tdb, all, *t); tdb.deleteT (*t); - if (conf.get ("echo.command", true)) - out << "Deleting task " - << t->getId () - << " " - << t->getDescription () - << std::endl; + out << "Deleting recurring task " + << t->getId () + << " '" + << t->getDescription () + << "'" + << std::endl; } - - break; // No point continuing the loop. + } + else + { + tdb.deleteT (*t); + if (conf.get ("echo.command", true)) + out << "Deleting task " + << t->getId () + << " '" + << t->getDescription () + << "'" + << std::endl; } } + else + out << "Task not deleted." << std::endl; } - else - out << "Task not deleted." << std::endl; return out.str (); } @@ -508,81 +505,66 @@ std::string handleDelete (TDB& tdb, T& task, Config& conf) //////////////////////////////////////////////////////////////////////////////// std::string handleStart (TDB& tdb, T& task, Config& conf) { + std::stringstream out; + std::vector all; tdb.pendingT (all); + filterSequence (all, task); - std::vector ::iterator it; - for (it = all.begin (); it != all.end (); ++it) + foreach (t, all) { - if (it->getId () == task.getId ()) + if (t->getAttribute ("start") == "") { - T original (*it); - std::stringstream out; + char startTime[16]; + sprintf (startTime, "%u", (unsigned int) time (NULL)); + t->setAttribute ("start", startTime); - if (original.getAttribute ("start") == "") - { - char startTime[16]; - sprintf (startTime, "%u", (unsigned int) time (NULL)); - original.setAttribute ("start", startTime); + tdb.modifyT (*t); - original.setId (task.getId ()); - tdb.modifyT (original); - - if (conf.get ("echo.command", true)) - out << "Started " - << original.getId () - << " " - << original.getDescription () - << std::endl; - nag (tdb, task, conf); - } - else - { - out << "Task " << task.getId () << " already started." << std::endl; - } - - return out.str (); + if (conf.get ("echo.command", true)) + out << "Started " + << t->getId () + << " '" + << t->getDescription () + << "'" + << std::endl; + nag (tdb, task, conf); + } + else + { + out << "Task " << t->getId () << " '" << t->getDescription () << "' already started." << std::endl; } } - throw std::string ("Task not found."); - return std::string (""); // To satisfy gcc. + return out.str (); } //////////////////////////////////////////////////////////////////////////////// std::string handleStop (TDB& tdb, T& task, Config& conf) { + std::stringstream out; + std::vector all; tdb.pendingT (all); + filterSequence (all, task); - std::vector ::iterator it; - for (it = all.begin (); it != all.end (); ++it) + foreach (t, all) { - if (it->getId () == task.getId ()) + if (t->getAttribute ("start") != "") { - T original (*it); - std::stringstream out; + t->removeAttribute ("start"); + tdb.modifyT (*t); - if (original.getAttribute ("start") != "") - { - original.removeAttribute ("start"); - original.setId (task.getId ()); - tdb.modifyT (original); - - if (conf.get ("echo.command", true)) - out << "Stopped " << original.getId () << " " << original.getDescription () << std::endl; - } - else - { - out << "Task " << task.getId () << " not started." << std::endl; - } - - return out.str (); + if (conf.get ("echo.command", true)) + out << "Stopped " << t->getId () << " '" << t->getDescription () << "'" << std::endl; + } + else + { + out << "Task " << t->getId () << " '" << t->getDescription () << "' not started." << std::endl; } } - throw std::string ("Task not found."); - return std::string (""); // To satisfy gcc. + return out.str (); } //////////////////////////////////////////////////////////////////////////////// @@ -590,30 +572,30 @@ std::string handleDone (TDB& tdb, T& task, Config& conf) { std::stringstream out; - if (!tdb.completeT (task)) - throw std::string ("Could not mark task as completed."); - - // Now update mask in parent. std::vector all; tdb.allPendingT (all); - foreach (t, all) - { - if (t->getId () == task.getId ()) - { - if (conf.get ("echo.command", true)) - out << "Completed " - << t->getId () - << " " - << t->getDescription () - << std::endl; + std::vector filtered = all; + filterSequence (filtered, task); - t->setStatus (T::completed); - updateRecurrenceMask (tdb, all, *t); - break; - } + foreach (t, filtered) + { + t->setStatus (T::completed); + if (!tdb.completeT (*t)) + throw std::string ("Could not mark task as completed."); + + // Now update mask in parent. + if (conf.get ("echo.command", true)) + out << "Completed " + << t->getId () + << " '" + << t->getDescription () + << "'" + << std::endl; + + updateRecurrenceMask (tdb, all, *t); + nag (tdb, task, conf); } - nag (tdb, task, conf); return out.str (); } @@ -677,164 +659,51 @@ std::string handleExport (TDB& tdb, T& task, Config& conf) //////////////////////////////////////////////////////////////////////////////// std::string handleModify (TDB& tdb, T& task, Config& conf) { + int count = 0; std::stringstream out; std::vector all; tdb.allPendingT (all); - // Lookup the complete task. - T complete = findT (task.getId (), all); - - // Perform some logical consistency checks. - if (task.getAttribute ("recur") != "" && - task.getAttribute ("due") == "" && - complete.getAttribute ("due") == "") - throw std::string ("You cannot specify a recurring task without a due date."); - - if (task.getAttribute ("until") != "" && - task.getAttribute ("recur") == "" && - complete.getAttribute ("recur") == "") - throw std::string ("You cannot specify an until date for a non-recurring task."); - - int count = 0; - std::vector ::iterator it; - for (it = all.begin (); it != all.end (); ++it) + std::vector filtered = all; + filterSequence (filtered, task); + foreach (seq, filtered) { - if (it->getId () == complete.getId () || // Self - (complete.getAttribute ("parent") != "" && - it->getAttribute ("parent") == complete.getAttribute ("parent")) || // Sibling - it->getUUID () == complete.getAttribute ("parent")) // Parent + // Perform some logical consistency checks. + if (task.getAttribute ("recur") != "" && + task.getAttribute ("due") == "" && + seq->getAttribute ("due") == "") + throw std::string ("You cannot specify a recurring task without a due date."); + + if (task.getAttribute ("until") != "" && + task.getAttribute ("recur") == "" && + seq->getAttribute ("recur") == "") + throw std::string ("You cannot specify an until date for a non-recurring task."); + + // Make all changes. + foreach (other, all) { - T original (*it); - - // A non-zero value forces a file write. - int changes = 0; - - // Apply a new description, if any. - if (task.getDescription () != "") + if (other->getId () == seq->getId () || // Self + (seq->getAttribute ("parent") != "" && + seq->getAttribute ("parent") == other->getAttribute ("parent")) || // Sibling + other->getUUID () == seq->getAttribute ("parent")) // Parent { - original.setDescription (task.getDescription ()); - ++changes; + // A non-zero value forces a file write. + int changes = 0; + + // Apply other deltas. + changes += deltaDescription (*other, task); + changes += deltaTags (*other, task); + changes += deltaAttributes (*other, task); + changes += deltaSubstitutions (*other, task); + + if (changes) + tdb.modifyT (*other); + + ++count; } - - // Apply or remove tags, if any. - std::vector tags; - task.getTags (tags); - for (unsigned int i = 0; i < tags.size (); ++i) - { - if (tags[i][0] == '+') - original.addTag (tags[i].substr (1, std::string::npos)); - else - original.addTag (tags[i]); - - ++changes; - } - - task.getRemoveTags (tags); - for (unsigned int i = 0; i < tags.size (); ++i) - { - if (tags[i][0] == '-') - original.removeTag (tags[i].substr (1, std::string::npos)); - else - original.removeTag (tags[i]); - - ++changes; - } - - // Apply or remove attributes, if any. - std::map attributes; - task.getAttributes (attributes); - foreach (i, attributes) - { - if (i->second == "") - original.removeAttribute (i->first); - else - { - original.setAttribute (i->first, i->second); - - // If a "recur" attribute is added, upgrade to a recurring task. - if (i->first == "recur") - original.setStatus (T::recurring); - } - - ++changes; - } - - std::string from; - std::string to; - bool global; - task.getSubstitution (from, to, global); - if (from != "") - { - std::string description = original.getDescription (); - size_t pattern; - - if (global) - { - // Perform all subs on description. - while ((pattern = description.find (from)) != std::string::npos) - { - description.replace (pattern, from.length (), to); - ++changes; - } - - original.setDescription (description); - - // Perform all subs on annotations. - std::map annotations; - original.getAnnotations (annotations); - std::map ::iterator it; - for (it = annotations.begin (); it != annotations.end (); ++it) - { - while ((pattern = it->second.find (from)) != std::string::npos) - { - it->second.replace (pattern, from.length (), to); - ++changes; - } - } - - original.setAnnotations (annotations); - } - else - { - // Perform first description substitution. - if ((pattern = description.find (from)) != std::string::npos) - { - description.replace (pattern, from.length (), to); - original.setDescription (description); - ++changes; - } - // Failing that, perform the first annotation substitution. - else - { - std::map annotations; - original.getAnnotations (annotations); - - std::map ::iterator it; - for (it = annotations.begin (); it != annotations.end (); ++it) - { - if ((pattern = it->second.find (from)) != std::string::npos) - { - it->second.replace (pattern, from.length (), to); - ++changes; - break; - } - } - - original.setAnnotations (annotations); - } - } - } - - if (changes) - tdb.modifyT (original); - - ++count; } } - if (count == 0) - throw std::string ("Task not found."); - if (conf.get ("echo.command", true)) out << "Modified " << count << " task" << (count == 1 ? "" : "s") << std::endl; @@ -844,92 +713,47 @@ std::string handleModify (TDB& tdb, T& task, Config& conf) //////////////////////////////////////////////////////////////////////////////// std::string handleAppend (TDB& tdb, T& task, Config& conf) { + int count = 0; std::stringstream out; std::vector all; tdb.allPendingT (all); - // Lookup the complete task. - T complete = findT (task.getId (), all); - - int count = 0; - std::vector ::iterator it; - for (it = all.begin (); it != all.end (); ++it) + std::vector filtered = all; + filterSequence (filtered, task); + foreach (seq, filtered) { - if (it->getId () == complete.getId () || // Self - (complete.getAttribute ("parent") != "" && - it->getAttribute ("parent") == complete.getAttribute ("parent")) || // Sibling - it->getUUID () == complete.getAttribute ("parent")) // Parent + foreach (other, all) { - T original (*it); - - // A non-zero value forces a file write. - int changes = 0; - - // Apply a new description, if any. - if (task.getDescription () != "") + if (other->getId () == seq->getId () || // Self + (seq->getAttribute ("parent") != "" && + seq->getAttribute ("parent") == other->getAttribute ("parent")) || // Sibling + other->getUUID () == seq->getAttribute ("parent")) // Parent { - original.setDescription (original.getDescription () + - " " + - task.getDescription ()); - ++changes; + // A non-zero value forces a file write. + int changes = 0; + + // Apply other deltas. + changes += deltaAppend (*other, task); + changes += deltaTags (*other, task); + changes += deltaAttributes (*other, task); + + if (changes) + { + tdb.modifyT (*other); + + if (conf.get ("echo.command", true)) + out << "Appended '" + << task.getDescription () + << "' to task " + << other->getId () + << std::endl; + } + + ++count; } - - // Apply or remove tags, if any. - std::vector tags; - task.getTags (tags); - for (unsigned int i = 0; i < tags.size (); ++i) - { - if (tags[i][0] == '+') - original.addTag (tags[i].substr (1, std::string::npos)); - else - original.addTag (tags[i]); - - ++changes; - } - - task.getRemoveTags (tags); - for (unsigned int i = 0; i < tags.size (); ++i) - { - if (tags[i][0] == '-') - original.removeTag (tags[i].substr (1, std::string::npos)); - else - original.removeTag (tags[i]); - - ++changes; - } - - // Apply or remove attributes, if any. - std::map attributes; - task.getAttributes (attributes); - foreach (i, attributes) - { - if (i->second == "") - original.removeAttribute (i->first); - else - original.setAttribute (i->first, i->second); - - ++changes; - } - - if (changes) - { - tdb.modifyT (original); - - if (conf.get ("echo.command", true)) - out << "Appended '" - << task.getDescription () - << "' to task " - << original.getId () - << std::endl; - } - - ++count; } } - if (count == 0) - throw std::string ("Task not found."); - if (conf.get ("echo.command", true)) out << "Modified " << count << " task" << (count == 1 ? "" : "s") << std::endl; @@ -1030,28 +854,22 @@ std::string handleAnnotate (TDB& tdb, T& task, Config& conf) std::stringstream out; std::vector all; tdb.pendingT (all); + filterSequence (all, task); - std::vector ::iterator it; - for (it = all.begin (); it != all.end (); ++it) + foreach (t, all) { - if (it->getId () == task.getId ()) - { - it->addAnnotation (task.getDescription ()); - tdb.modifyT (*it); + t->addAnnotation (task.getDescription ()); + tdb.modifyT (*t); - if (conf.get ("echo.command", true)) - out << "Annotated " - << task.getId () - << " with '" - << task.getDescription () - << "'" - << std::endl; - - return out.str (); - } + if (conf.get ("echo.command", true)) + out << "Annotated " + << t->getId () + << " with '" + << t->getDescription () + << "'" + << std::endl; } - throw std::string ("Task not found."); return out.str (); } @@ -1067,3 +885,156 @@ T findT (int id, const std::vector & all) } //////////////////////////////////////////////////////////////////////////////// +int deltaAppend (T& task, T& delta) +{ + if (delta.getDescription () != "") + { + task.setDescription ( + task.getDescription () + + " " + + delta.getDescription ()); + + return 1; + } + + return 0; +} + +//////////////////////////////////////////////////////////////////////////////// +int deltaDescription (T& task, T& delta) +{ + if (delta.getDescription () != "") + { + task.setDescription (delta.getDescription ()); + return 1; + } + + return 0; +} + +//////////////////////////////////////////////////////////////////////////////// +int deltaTags (T& task, T& delta) +{ + int changes = 0; + + // Apply or remove tags, if any. + std::vector tags; + delta.getTags (tags); + for (unsigned int i = 0; i < tags.size (); ++i) + { + if (tags[i][0] == '+') + task.addTag (tags[i].substr (1, std::string::npos)); + else + task.addTag (tags[i]); + + ++changes; + } + + delta.getRemoveTags (tags); + for (unsigned int i = 0; i < tags.size (); ++i) + { + if (tags[i][0] == '-') + task.removeTag (tags[i].substr (1, std::string::npos)); + else + task.removeTag (tags[i]); + + ++changes; + } + + return changes; +} + +//////////////////////////////////////////////////////////////////////////////// +int deltaAttributes (T& task, T& delta) +{ + int changes = 0; + + std::map attributes; + delta.getAttributes (attributes); + foreach (i, attributes) + { + if (i->second == "") + task.removeAttribute (i->first); + else + task.setAttribute (i->first, i->second); + + ++changes; + } + + return changes; +} + +//////////////////////////////////////////////////////////////////////////////// +int deltaSubstitutions (T& task, T& delta) +{ + int changes = 0; + std::string from; + std::string to; + bool global; + delta.getSubstitution (from, to, global); + + if (from != "") + { + std::string description = task.getDescription (); + size_t pattern; + + if (global) + { + // Perform all subs on description. + while ((pattern = description.find (from)) != std::string::npos) + { + description.replace (pattern, from.length (), to); + ++changes; + } + + task.setDescription (description); + + // Perform all subs on annotations. + std::map annotations; + task.getAnnotations (annotations); + std::map ::iterator it; + for (it = annotations.begin (); it != annotations.end (); ++it) + { + while ((pattern = it->second.find (from)) != std::string::npos) + { + it->second.replace (pattern, from.length (), to); + ++changes; + } + } + + task.setAnnotations (annotations); + } + else + { + // Perform first description substitution. + if ((pattern = description.find (from)) != std::string::npos) + { + description.replace (pattern, from.length (), to); + task.setDescription (description); + ++changes; + } + // Failing that, perform the first annotation substitution. + else + { + std::map annotations; + task.getAnnotations (annotations); + + std::map ::iterator it; + for (it = annotations.begin (); it != annotations.end (); ++it) + { + if ((pattern = it->second.find (from)) != std::string::npos) + { + it->second.replace (pattern, from.length (), to); + ++changes; + break; + } + } + + task.setAnnotations (annotations); + } + } + } + return changes; +} + +//////////////////////////////////////////////////////////////////////////////// diff --git a/src/parse.cpp b/src/parse.cpp index 1d538624f..adb71218d 100644 --- a/src/parse.cpp +++ b/src/parse.cpp @@ -301,6 +301,9 @@ static bool validAttribute ( //////////////////////////////////////////////////////////////////////////////// static bool validId (const std::string& input) { + if (input.length () == 0) + return false; + for (size_t i = 0; i < input.length (); ++i) if (!::isdigit (input[i])) return false; @@ -308,6 +311,56 @@ static bool validId (const std::string& input) return true; } +//////////////////////////////////////////////////////////////////////////////// +// 1,2-4,6 +static bool validSequence ( + const std::string& input, + std::vector & ids) +{ + std::vector ranges; + split (ranges, input, ','); + + std::vector ::iterator it; + for (it = ranges.begin (); it != ranges.end (); ++it) + { + std::vector range; + split (range, *it, '-'); + + switch (range.size ()) + { + case 1: + if (! validId (range[0])) + return false; + + int id = ::atoi (range[0].c_str ()); + ids.push_back (id); + break; + + case 2: + { + if (! validId (range[0]) || + ! validId (range[1])) + return false; + + int low = ::atoi (range[0].c_str ()); + int high = ::atoi (range[1].c_str ()); + if (low >= high) + return false; + + for (int i = low; i <= high; ++i) + ids.push_back (i); + } + break; + + default: + return false; + break; + } + } + + return ids.size () ? true : false; +} + //////////////////////////////////////////////////////////////////////////////// static bool validTag (const std::string& input) { @@ -392,15 +445,23 @@ bool validDuration (std::string& input) } //////////////////////////////////////////////////////////////////////////////// -// Token Distinguishing characteristic -// ------- ----------------------------- -// command first positional -// id \d+ -// description default, accumulate -// substitution /\w+/\w*/ -// tags [-+]\w+ -// attributes \w+:.+ +// Token EBNF +// ------- ---------------------------------- +// command first non-id recognized argument // +// substitution ::= "/" from "/" to "/g" +// | "/" from "/" to "/" ; +// +// tags ::= "+" word +// | "-" word ; +// +// attributes ::= word ":" value +// | word ":" +// +// sequence ::= \d+ "," sequence +// | \d+ "-" \d+ ; +// +// description (whatever isn't one of the above) void parse ( std::vector & args, std::string& command, @@ -409,6 +470,9 @@ void parse ( { command = ""; + bool foundSequence = false; + bool foundSomethingAfterSequence = false; + std::string descCandidate = ""; for (size_t i = 0; i < args.size (); ++i) { @@ -422,16 +486,24 @@ void parse ( std::string from; std::string to; bool global; + std::vector sequence; // An id is the first argument found that contains all digits. - if (lowerCase (command) != "add" && // "add" doesn't require an ID - task.getId () == 0 && - validId (arg)) - task.setId (::atoi (arg.c_str ())); + if (lowerCase (command) != "add" && // "add" doesn't require an ID + validSequence (arg, sequence) && + ! foundSomethingAfterSequence) + { + foundSequence = true; + foreach (id, sequence) + task.addId (*id); + } // Tags begin with + or - and contain arbitrary text. else if (validTag (arg)) { + if (foundSequence) + foundSomethingAfterSequence = true; + if (arg[0] == '+') task.addTag (arg.substr (1, std::string::npos)); else if (arg[0] == '-') @@ -442,6 +514,9 @@ void parse ( // value. else if ((colon = arg.find (":")) != std::string::npos) { + if (foundSequence) + foundSomethingAfterSequence = true; + std::string name = lowerCase (arg.substr (0, colon)); std::string value = arg.substr (colon + 1, std::string::npos); @@ -464,12 +539,18 @@ void parse ( // Substitution of description text. else if (validSubstitution (arg, from, to, global)) { + if (foundSequence) + foundSomethingAfterSequence = true; + task.setSubstitution (from, to, global); } // Command. else if (command == "") { + if (foundSequence) + foundSomethingAfterSequence = true; + std::string l = lowerCase (arg); if (isCommand (l) && validCommand (l)) command = l; @@ -484,6 +565,9 @@ void parse ( // Anything else is just considered description. else { + if (foundSequence) + foundSomethingAfterSequence = true; + if (descCandidate.length ()) descCandidate += " "; descCandidate += arg; diff --git a/src/report.cpp b/src/report.cpp index 53bf00662..ba42bd27b 100644 --- a/src/report.cpp +++ b/src/report.cpp @@ -46,6 +46,53 @@ #include #endif +//////////////////////////////////////////////////////////////////////////////// +void filterSequence (std::vector& all, T& task) +{ + std::vector sequence = task.getAllIds (); + + std::vector filtered; + std::vector ::iterator t; + for (t = all.begin (); t != all.end (); ++t) + { + std::vector ::iterator s; + for (s = sequence.begin (); s != sequence.end (); ++s) + if (t->getId () == *s) + filtered.push_back (*t); + } + + if (sequence.size () != filtered.size ()) + { + std::vector filteredSequence; + std::vector ::iterator fs; + for (fs = filtered.begin (); fs != filtered.end (); ++fs) + filteredSequence.push_back (fs->getId ()); + + std::vector left; + std::vector right; + listDiff (filteredSequence, sequence, left, right); + if (left.size ()) + throw std::string ("Sequence filtering error - please report this error"); + + if (right.size ()) + { + std::stringstream out; + out << "Task"; + + if (right.size () > 1) out << "s"; + + std::vector ::iterator s; + for (s = right.begin (); s != right.end (); ++s) + out << " " << *s; + + out << " not found"; + throw out.str (); + } + } + + all = filtered; +} + //////////////////////////////////////////////////////////////////////////////// void filter (std::vector& all, T& task) { @@ -265,47 +312,53 @@ std::string handleInfo (TDB& tdb, T& task, Config& conf) std::vector tasks; tdb.allPendingT (tasks); - Table table; - table.setTableWidth (width); - table.setDateFormat (conf.get ("dateformat", "m/d/Y")); - - table.addColumn ("Name"); - table.addColumn ("Value"); - - if (conf.get ("color", true) || conf.get (std::string ("_forcecolor"), false)) - { - table.setColumnUnderline (0); - table.setColumnUnderline (1); - } - else - table.setTableDashedUnderline (); - - table.setColumnWidth (0, Table::minimum); - table.setColumnWidth (1, Table::flexible); - - table.setColumnJustification (0, Table::left); - table.setColumnJustification (1, Table::left); - // Find the task. + int count = 0; for (unsigned int i = 0; i < tasks.size (); ++i) { T refTask (tasks[i]); - if (refTask.getId () == task.getId ()) + if (refTask.getId () == task.getId () || task.sequenceContains (refTask.getId ())) { - Date now; + ++count; + + Table table; + table.setTableWidth (width); + table.setDateFormat (conf.get ("dateformat", "m/d/Y")); + + table.addColumn ("Name"); + table.addColumn ("Value"); + + if (conf.get ("color", true) || conf.get (std::string ("_forcecolor"), false)) + { + table.setColumnUnderline (0); + table.setColumnUnderline (1); + } + else + table.setTableDashedUnderline (); + + table.setColumnWidth (0, Table::minimum); + table.setColumnWidth (1, Table::flexible); + + table.setColumnJustification (0, Table::left); + table.setColumnJustification (1, Table::left); + Date now; int row = table.addRow (); table.addCell (row, 0, "ID"); table.addCell (row, 1, refTask.getId ()); + std::string status = refTask.getStatus () == T::pending ? "Pending" + : refTask.getStatus () == T::completed ? "Completed" + : refTask.getStatus () == T::deleted ? "Deleted" + : refTask.getStatus () == T::recurring ? "Recurring" + : ""; + if (refTask.getAttribute ("parent") != "") + status += " (Recurring)"; + row = table.addRow (); table.addCell (row, 0, "Status"); - table.addCell (row, 1, ( refTask.getStatus () == T::pending ? "Pending" - : refTask.getStatus () == T::completed ? "Completed" - : refTask.getStatus () == T::deleted ? "Deleted" - : refTask.getStatus () == T::recurring ? "Recurring" - : "")); + table.addCell (row, 1, status); std::string description = refTask.getDescription (); std::string when; @@ -336,26 +389,36 @@ std::string handleInfo (TDB& tdb, T& task, Config& conf) table.addCell (row, 1, refTask.getAttribute ("priority")); } - if (refTask.getStatus () == T::recurring) + if (refTask.getStatus () == T::recurring || + refTask.getAttribute ("parent") != "") { - row = table.addRow (); - table.addCell (row, 0, "Recurrence"); - table.addCell (row, 1, refTask.getAttribute ("recur")); + if (refTask.getAttribute ("recur") != "") + { + row = table.addRow (); + table.addCell (row, 0, "Recurrence"); + table.addCell (row, 1, refTask.getAttribute ("recur")); + } - row = table.addRow (); - table.addCell (row, 0, "Recur until"); - table.addCell (row, 1, refTask.getAttribute ("until")); + if (refTask.getAttribute ("until") != "") + { + row = table.addRow (); + table.addCell (row, 0, "Recur until"); + table.addCell (row, 1, refTask.getAttribute ("until")); + } - row = table.addRow (); - table.addCell (row, 0, "Mask"); - table.addCell (row, 1, refTask.getAttribute ("mask")); - } + if (refTask.getAttribute ("mask") != "") + { + row = table.addRow (); + table.addCell (row, 0, "Mask"); + table.addCell (row, 1, refTask.getAttribute ("mask")); + } - if (refTask.getAttribute ("parent") != "") - { - row = table.addRow (); - table.addCell (row, 0, "Parent task"); - table.addCell (row, 1, refTask.getAttribute ("parent")); + if (refTask.getAttribute ("parent") != "") + { + row = table.addRow (); + table.addCell (row, 0, "Parent task"); + table.addCell (row, 1, refTask.getAttribute ("parent")); + } row = table.addRow (); table.addCell (row, 0, "Mask Index"); @@ -440,14 +503,14 @@ std::string handleInfo (TDB& tdb, T& task, Config& conf) } table.addCell (row, 1, entry + " (" + age + ")"); + + out << optionalBlankLine (conf) + << table.render () + << std::endl; } } - if (table.rowCount ()) - out << optionalBlankLine (conf) - << table.render () - << std::endl; - else + if (! count) out << "No matches." << std::endl; return out.str (); @@ -1038,6 +1101,7 @@ std::string handleReportGHistory (TDB& tdb, T& task, Config& conf) if (task.getStatus () == T::deleted) { epoch = monthlyEpoch (task.getAttribute ("end")); + groups[epoch] = 0; if (deletedGroup.find (epoch) != deletedGroup.end ()) deletedGroup[epoch] = deletedGroup[epoch] + 1; @@ -1047,6 +1111,7 @@ std::string handleReportGHistory (TDB& tdb, T& task, Config& conf) else if (task.getStatus () == T::completed) { epoch = monthlyEpoch (task.getAttribute ("end")); + groups[epoch] = 0; if (completedGroup.find (epoch) != completedGroup.end ()) completedGroup[epoch] = completedGroup[epoch] + 1; diff --git a/src/task.cpp b/src/task.cpp index 6d25adbb0..5dca26c00 100644 --- a/src/task.cpp +++ b/src/task.cpp @@ -230,7 +230,13 @@ static std::string longUsage (Config& conf) { std::stringstream out; out << shortUsage (conf) - << "ID is the numeric identifier displayed by the 'task list' command." << "\n" + << "ID is the numeric identifier displayed by the 'task list' command. " + << "You can specify multiple IDs for task commands, and multiple tasks " + << "will be affected. To specify multiple IDs make sure you use one " + << "of these forms:" << "\n" + << " task delete 1,2,3" << "\n" + << " task info 1-3" << "\n" + << " task pri:H 1,2-5,19" << "\n" << "\n" << "Tags are arbitrary words, any quantity:" << "\n" << " +tag The + means add the tag" << "\n" @@ -339,7 +345,7 @@ int main (int argc, char** argv) catch (std::string& error) { - std::cerr << error << std::endl; + std::cout << error << std::endl; return -1; } diff --git a/src/task.h b/src/task.h index a1c9559a2..b85da7119 100644 --- a/src/task.h +++ b/src/task.h @@ -90,8 +90,14 @@ std::string handleUndo (TDB&, T&, Config&); std::string handleColor (Config&); std::string handleAnnotate (TDB&, T&, Config&); T findT (int, const std::vector &); +int deltaAppend (T&, T&); +int deltaDescription (T&, T&); +int deltaTags (T&, T&); +int deltaAttributes (T&, T&); +int deltaSubstitutions (T&, T&); // report.cpp +void filterSequence (std::vector&, T&); void filter (std::vector&, T&); std::string handleInfo (TDB&, T&, Config&); std::string handleCompleted (TDB&, T&, Config&); @@ -152,4 +158,45 @@ void autoColorize (T&, Text::color&, Text::color&, Config&); // import.cpp std::string handleImport (TDB&, T&, Config&); +// list template +/////////////////////////////////////////////////////////////////////////////// +template void listDiff ( + const T& left, const T& right, T& leftOnly, T& rightOnly) +{ + leftOnly.clear (); + rightOnly.clear (); + + for (unsigned int l = 0; l < left.size (); ++l) + { + bool found = false; + for (unsigned int r = 0; r < right.size (); ++r) + { + if (left[l] == right[r]) + { + found = true; + break; + } + } + + if (!found) + leftOnly.push_back (left[l]); + } + + for (unsigned int r = 0; r < right.size (); ++r) + { + bool found = false; + for (unsigned int l = 0; l < left.size (); ++l) + { + if (left[l] == right[r]) + { + found = true; + break; + } + } + + if (!found) + rightOnly.push_back (right[r]); + } +} + //////////////////////////////////////////////////////////////////////////////// diff --git a/src/tests/.gitignore b/src/tests/.gitignore index f25e411bc..6f7048780 100644 --- a/src/tests/.gitignore +++ b/src/tests/.gitignore @@ -5,3 +5,4 @@ date.t duration.t text.t autocomplete.t +parse.t diff --git a/src/tests/Makefile b/src/tests/Makefile index 0650a8a92..a71c8633e 100644 --- a/src/tests/Makefile +++ b/src/tests/Makefile @@ -1,4 +1,5 @@ -PROJECT = t.t tdb.t date.t duration.t t.benchmark.t text.t autocomplete.t +PROJECT = t.t tdb.t date.t duration.t t.benchmark.t text.t autocomplete.t \ + parse.t CFLAGS = -I. -I.. -Wall -pedantic -ggdb3 -fno-rtti LFLAGS = -L/usr/local/lib OBJECTS = ../TDB.o ../T.o ../parse.o ../text.o ../Date.o ../util.o ../Config.o @@ -38,3 +39,6 @@ text.t: text.t.o $(OBJECTS) test.o autocomplete.t: autocomplete.t.o $(OBJECTS) test.o g++ autocomplete.t.o $(OBJECTS) test.o $(LFLAGS) -o autocomplete.t +parse.t: parse.t.o $(OBJECTS) test.o + g++ parse.t.o $(OBJECTS) test.o $(LFLAGS) -o parse.t + diff --git a/src/tests/confirmation.t b/src/tests/confirmation.t index 99bc8e378..955cace13 100755 --- a/src/tests/confirmation.t +++ b/src/tests/confirmation.t @@ -51,49 +51,49 @@ qx{../task rc:confirm.rc add foo} for 1 .. 10; # Test the various forms of "yes". my $output = qx{echo "yes" | ../task rc:confirm.rc del 1}; -like ($output, qr/Permanently delete task\? \(y\/n\)/, 'confirmation - yes works'); +like ($output, qr/Permanently delete task 1 'foo'\? \(y\/n\)/, 'confirmation - yes works'); unlike ($output, qr/Task not deleted\./, 'confirmation - yes works'); $output = qx{echo "ye" | ../task rc:confirm.rc del 2}; -like ($output, qr/Permanently delete task\? \(y\/n\)/, 'confirmation - ye works'); +like ($output, qr/Permanently delete task 2 'foo'\? \(y\/n\)/, 'confirmation - ye works'); unlike ($output, qr/Task not deleted\./, 'confirmation - ye works'); $output = qx{echo "y" | ../task rc:confirm.rc del 3}; -like ($output, qr/Permanently delete task\? \(y\/n\)/, 'confirmation - y works'); +like ($output, qr/Permanently delete task 3 'foo'\? \(y\/n\)/, 'confirmation - y works'); unlike ($output, qr/Task not deleted\./, 'confirmation - y works'); $output = qx{echo "YES" | ../task rc:confirm.rc del 4}; -like ($output, qr/Permanently delete task\? \(y\/n\)/, 'confirmation - YES works'); +like ($output, qr/Permanently delete task 4 'foo'\? \(y\/n\)/, 'confirmation - YES works'); unlike ($output, qr/Task not deleted\./, 'confirmation - YES works'); $output = qx{echo "YE" | ../task rc:confirm.rc del 5}; -like ($output, qr/Permanently delete task\? \(y\/n\)/, 'confirmation - YE works'); +like ($output, qr/Permanently delete task 5 'foo'\? \(y\/n\)/, 'confirmation - YE works'); unlike ($output, qr/Task not deleted\./, 'confirmation - YE works'); $output = qx{echo "Y" | ../task rc:confirm.rc del 6}; -like ($output, qr/Permanently delete task\? \(y\/n\)/, 'confirmation - Y works'); +like ($output, qr/Permanently delete task 6 'foo'\? \(y\/n\)/, 'confirmation - Y works'); unlike ($output, qr/Task not deleted\./, 'confirmation - Y works'); # Test the various forms of "no". $output = qx{echo "no" | ../task rc:confirm.rc del 7}; -like ($output, qr/Permanently delete task\? \(y\/n\)/, 'confirmation - no works'); +like ($output, qr/Permanently delete task 7 'foo'\? \(y\/n\)/, 'confirmation - no works'); like ($output, qr/Task not deleted\./, 'confirmation - no works'); $output = qx{echo "n" | ../task rc:confirm.rc del 7}; -like ($output, qr/Permanently delete task\? \(y\/n\)/, 'confirmation - n works'); +like ($output, qr/Permanently delete task 7 'foo'\? \(y\/n\)/, 'confirmation - n works'); like ($output, qr/Task not deleted\./, 'confirmation - n works'); $output = qx{echo "NO" | ../task rc:confirm.rc del 7}; -like ($output, qr/Permanently delete task\? \(y\/n\)/, 'confirmation - NO works'); +like ($output, qr/Permanently delete task 7 'foo'\? \(y\/n\)/, 'confirmation - NO works'); like ($output, qr/Task not deleted\./, 'confirmation - NO works'); $output = qx{echo "N" | ../task rc:confirm.rc del 7}; -like ($output, qr/Permanently delete task\? \(y\/n\)/, 'confirmation - N works'); +like ($output, qr/Permanently delete task 7 'foo'\? \(y\/n\)/, 'confirmation - N works'); like ($output, qr/Task not deleted\./, 'confirmation - N works'); # Test newlines. $output = qx{cat response.txt | ../task rc:confirm.rc del 7}; -like ($output, qr/(Permanently delete task\? \(y\/n\)) \1 \1/, 'confirmation - \n re-prompt works'); +like ($output, qr/(Permanently delete task 7 'foo'\? \(y\/n\)) \1 \1/, 'confirmation - \n re-prompt works'); # Cleanup. unlink 'pending.data'; diff --git a/src/tests/delete.t b/src/tests/delete.t index d3a34a84c..92a1f0414 100755 --- a/src/tests/delete.t +++ b/src/tests/delete.t @@ -57,7 +57,7 @@ like ($output, qr/^No matches/, 'No matches'); ok (-r 'completed.data', 'completed.data created'); $output = qx{../task rc:undelete.rc undelete 1}; -like ($output, qr/reliably undeleted/, 'can only be reliable undeleted...'); +like ($output, qr/Task 1 not found/, 'Task 1 not found'); $output = qx{../task rc:undelete.rc info 1}; like ($output, qr/No matches./, 'no matches'); diff --git a/src/tests/parse.t.cpp b/src/tests/parse.t.cpp new file mode 100644 index 000000000..3065cd952 --- /dev/null +++ b/src/tests/parse.t.cpp @@ -0,0 +1,134 @@ +//////////////////////////////////////////////////////////////////////////////// +// 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 +// +//////////////////////////////////////////////////////////////////////////////// +#include +#include "task.h" +#include "test.h" + +//////////////////////////////////////////////////////////////////////////////// +int main (int argc, char** argv) +{ + UnitTest t (18); + + std::vector args; + std::string command; + + Config conf; + conf.set ("dateformat", "m/d/Y"); + + { + T task; + split (args, "add foo", ' '); + parse (args, command, task, conf); + t.is (command, "add", "(1) command found"); + t.is (task.getId (), 0, "(1) zero id on add"); + t.is (task.getDescription (), "foo", "(1) correct description"); + } + + { + T task; + split (args, "delete 1,3-5,7", ' '); + parse (args, command, task, conf); + std::vector sequence = task.getAllIds (); + t.is (sequence.size (), (size_t)5, "(2) sequence length"); + if (sequence.size () == 5) + { + t.is (sequence[0], 1, "(2) sequence[0] == 1"); + t.is (sequence[1], 3, "(2) sequence[1] == 3"); + t.is (sequence[2], 4, "(2) sequence[2] == 4"); + t.is (sequence[3], 5, "(2) sequence[3] == 5"); + t.is (sequence[4], 7, "(2) sequence[4] == 7"); + } + else + { + t.fail ("(2) sequence[0] == 1"); + t.fail ("(2) sequence[1] == 3"); + t.fail ("(2) sequence[2] == 4"); + t.fail ("(2) sequence[3] == 5"); + t.fail ("(2) sequence[4] == 7"); + } + } + + { + T task; + split (args, "delete 1,2 3,4", ' '); + parse (args, command, task, conf); + std::vector sequence = task.getAllIds (); + t.is (sequence.size (), (size_t)4, "(3) sequence length"); + if (sequence.size () == 4) + { + t.is (sequence[0], 1, "(3) sequence[0] == 1"); + t.is (sequence[1], 2, "(3) sequence[1] == 2"); + t.is (sequence[2], 3, "(3) sequence[2] == 3"); + t.is (sequence[3], 4, "(3) sequence[3] == 4"); + } + else + { + t.fail ("(3) sequence[0] == 1"); + t.fail ("(3) sequence[1] == 2"); + t.fail ("(3) sequence[2] == 3"); + t.fail ("(3) sequence[3] == 4"); + } + } + + { + T task; + split (args, "1 There are 7 days in a week", ' '); + parse (args, command, task, conf); + std::vector sequence = task.getAllIds (); + t.is (sequence.size (), (size_t)1, "(4) sequence length"); + if (sequence.size () == 1) + { + t.is (sequence[0], 1, "(4) sequence[0] == 1"); + } + else + { + t.fail ("(4) sequence[0] == 1"); + } + } + + { + T task; + args.clear (); + args.push_back ("1"); + args.push_back ("4-123 is back-ordered"); + parse (args, command, task, conf); + std::vector sequence = task.getAllIds (); + t.is (sequence.size (), (size_t)1, "(5) sequence length"); + if (sequence.size () == 1) + { + t.is (sequence[0], 1, "(5) sequence[0] == 1"); + } + else + { + t.fail ("(5) sequence[0] == 1"); + } + } + return 0; +} + +//////////////////////////////////////////////////////////////////////////////// + diff --git a/src/tests/substitute.t b/src/tests/substitute.t index 9365df970..16bb13d4d 100755 --- a/src/tests/substitute.t +++ b/src/tests/substitute.t @@ -28,7 +28,7 @@ use strict; use warnings; -use Test::More tests => 7; +use Test::More tests => 9; # Create the rc file. if (open my $fh, '>', 'subst.rc') @@ -58,6 +58,16 @@ qx{../task rc:subst.rc 1 /bar/BAR/g}; $output = qx{../task rc:subst.rc info 1}; like ($output, qr/BAR BAR BAR/, 'global substitution in annotation'); +qx{../task rc:subst.rc 1 /FOO/aaa/}; +qx{../task rc:subst.rc 1 /FOO/bbb/}; +qx{../task rc:subst.rc 1 /FOO/ccc/}; +$output = qx{../task rc:subst.rc info 1}; +like ($output, qr/aaa bbb ccc/, 'individual successive substitution in description'); + +qx{../task rc:subst.rc 1 /bbb//}; +$output = qx{../task rc:subst.rc info 1}; +like ($output, qr/aaa ccc/, 'word deletion in description'); + # Cleanup. unlink 'pending.data'; ok (!-r 'pending.data', 'Removed pending.data'); diff --git a/src/tests/undo.t b/src/tests/undo.t index 24803cd88..32131f21a 100755 --- a/src/tests/undo.t +++ b/src/tests/undo.t @@ -56,8 +56,8 @@ $output = qx{../task rc:undo.rc do 1; ../task rc:undo.rc list}; like ($output, qr/^No matches/, 'No matches'); $output = qx{../task rc:undo.rc undo 1; ../task rc:undo.rc info 1}; -like ($output, qr/Task 1 not found/, 'task not found'); -like ($output, qr/reliably undone/, 'can only be reliable undone...'); +like ($output, qr/Task 1 not found/, 'Task 1 not found'); +like ($output, qr/No matches/, 'No matches'); # Cleanup. ok (-r 'pending.data', 'Need to remove pending.data');