1 ////////////////////////////////////////////////////////////////////////////////
2 //
3 // Copyright 2006 - 2021, Tomas Babej, Paul Beckingham, Federico Hernandez.
4 //
5 // Permission is hereby granted, free of charge, to any person obtaining a copy
6 // of this software and associated documentation files (the "Software"), to deal
7 // in the Software without restriction, including without limitation the rights
8 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 // copies of the Software, and to permit persons to whom the Software is
10 // furnished to do so, subject to the following conditions:
11 //
12 // The above copyright notice and this permission notice shall be included
13 // in all copies or substantial portions of the Software.
14 //
15 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
16 // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
18 // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 // SOFTWARE.
22 //
23 // https://www.opensource.org/licenses/mit-license.php
24 //
25 ////////////////////////////////////////////////////////////////////////////////
26 
27 #include <cmake.h>
28 #include <CmdEdit.h>
29 #include <iostream>
30 #include <sstream>
31 #include <cstdlib>
32 #include <cstring>
33 #include <algorithm>
34 #include <unistd.h>
35 #include <cerrno>
36 #include <Datetime.h>
37 #include <Duration.h>
38 #include <Context.h>
39 #include <Lexer.h>
40 #include <Filter.h>
41 #include <Pig.h>
42 #include <shared.h>
43 #include <format.h>
44 #include <util.h>
45 #include <main.h>
46 #include <JSON.h>
47 
48 #define STRING_EDIT_START_MOD        "Start date modified."
49 #define STRING_EDIT_SCHED_MOD        "Scheduled date modified."
50 #define STRING_EDIT_DUE_MOD          "Due date modified."
51 #define STRING_EDIT_UNTIL_MOD        "Until date modified."
52 #define STRING_EDIT_WAIT_MOD         "Wait date modified."
53 
54 const std::string CmdEdit::ANNOTATION_EDIT_MARKER = "\n                     ";
55 
56 ////////////////////////////////////////////////////////////////////////////////
CmdEdit()57 CmdEdit::CmdEdit ()
58 {
59   _keyword               = "edit";
60   _usage                 = "task <filter> edit";
61   _description           = "Launches an editor to modify a task directly";
62   _read_only             = false;
63   _displays_id           = false;
64   _needs_gc              = false;
65   _uses_context          = true;
66   _accepts_filter        = true;
67   _accepts_modifications = false;
68   _accepts_miscellaneous = false;
69   _category              = Command::Category::operation;
70 }
71 
72 ////////////////////////////////////////////////////////////////////////////////
73 // Introducing the Silver Bullet.  This feature is the catch-all fixative for
74 // various other ills.  This is like opening up the hood and going in with a
75 // wrench.  To be used sparingly.
execute(std::string &)76 int CmdEdit::execute (std::string&)
77 {
78   // Filter the tasks.
79   handleUntil ();
80   handleRecurrence ();
81   Filter filter;
82   std::vector <Task> filtered;
83   filter.subset (filtered);
84 
85   if (! filtered.size ())
86   {
87     Context::getContext ().footnote ("No matches.");
88     return 1;
89   }
90 
91   unsigned int bulk = Context::getContext ().config.getInteger ("bulk");
92 
93   // If we are editing more than "bulk" tasks, ask for confirmation.
94   // Bulk = 0 denotes infinite bulk.
95   if ((filtered.size () > bulk) && (bulk != 0))
96     if (! confirm (format ("Do you wish to manually edit {1} tasks?", filtered.size ())))
97       return 2;
98 
99   // Find number of matching tasks.
100   for (auto& task : filtered)
101   {
102     auto result = editFile (task);
103     if (result == CmdEdit::editResult::error)
104       break;
105     else if (result == CmdEdit::editResult::changes)
106       Context::getContext ().tdb2.modify (task);
107   }
108 
109   return 0;
110 }
111 
112 ////////////////////////////////////////////////////////////////////////////////
findValue(const std::string & text,const std::string & name)113 std::string CmdEdit::findValue (
114   const std::string& text,
115   const std::string& name)
116 {
117   auto found = text.find (name);
118   if (found != std::string::npos)
119   {
120     auto eol = text.find ('\n', found + 1);
121     if (eol != std::string::npos)
122     {
123       std::string value = text.substr (
124         found + name.length (),
125         eol - (found + name.length ()));
126 
127       return Lexer::trim (value, "\t ");
128     }
129   }
130 
131   return "";
132 }
133 
134 ////////////////////////////////////////////////////////////////////////////////
findMultilineValue(const std::string & text,const std::string & startMarker,const std::string & endMarker)135 std::string CmdEdit::findMultilineValue (
136   const std::string& text,
137   const std::string& startMarker,
138   const std::string& endMarker)
139 {
140   auto start = text.find (startMarker);
141   if (start != std::string::npos)
142   {
143     auto end = text.find (endMarker, start);
144     if (end != std::string::npos)
145     {
146       std::string value = text.substr (start + startMarker.length (),
147                                        end - (start + startMarker.length ()));
148       return Lexer::trim (value, "\\\t ");
149     }
150   }
151   return "";
152 }
153 
154 ////////////////////////////////////////////////////////////////////////////////
findValues(const std::string & text,const std::string & name)155 std::vector <std::string> CmdEdit::findValues (
156   const std::string& text,
157   const std::string& name)
158 {
159   std::vector <std::string> results;
160   std::string::size_type found = 0;
161 
162   while (found != std::string::npos)
163   {
164     found = text.find (name, found + 1);
165     if (found != std::string::npos)
166     {
167       auto eol = text.find ('\n', found + 1);
168       if (eol != std::string::npos)
169       {
170         auto value = text.substr (
171           found + name.length (),
172           eol - (found + name.length ()));
173 
174         found = eol - 1;
175         results.push_back (Lexer::trim (value, "\t "));
176       }
177     }
178   }
179 
180   return results;
181 }
182 
183 ////////////////////////////////////////////////////////////////////////////////
formatDate(Task & task,const std::string & attribute,const std::string & dateformat)184 std::string CmdEdit::formatDate (
185   Task& task,
186   const std::string& attribute,
187   const std::string& dateformat)
188 {
189   auto value = task.get (attribute);
190   if (value.length ())
191     value = Datetime (value).toString (dateformat);
192 
193   return value;
194 }
195 
196 ////////////////////////////////////////////////////////////////////////////////
formatDuration(Task & task,const std::string & attribute)197 std::string CmdEdit::formatDuration (
198   Task& task,
199   const std::string& attribute)
200 {
201   auto value = task.get (attribute);
202   if (value.length ())
203     value = Duration (value).formatISO ();
204 
205   return value;
206 }
207 
208 ////////////////////////////////////////////////////////////////////////////////
formatTask(Task task,const std::string & dateformat)209 std::string CmdEdit::formatTask (Task task, const std::string& dateformat)
210 {
211   std::stringstream before;
212   auto verbose = Context::getContext ().verbose ("edit");
213 
214   if (verbose)
215     before << "# The 'task <id> edit' command allows you to modify all aspects of a task\n"
216               "# using a text editor.  Below is a representation of all the task details.\n"
217               "# Modify what you wish, and when you save and quit your editor,\n"
218               "# Taskwarrior will read this file, determine what changed, and apply\n"
219               "# those changes.  If you exit your editor without saving or making\n"
220               "# modifications, Taskwarrior will do nothing.\n"
221               "#\n"
222               "# Lines that begin with # represent data you cannot change, like ID.\n"
223               "# If you get too creative with your editing, Taskwarrior will send you\n"
224               "# back to the editor to try again.\n"
225               "#\n"
226               "# Should you find yourself in an endless loop, re-editing the same file,\n"
227               "# just quit the editor without making any changes.  Taskwarrior will\n"
228               "# notice this and stop the editing.\n"
229               "#\n";
230 
231   before << "# Name               Editable details\n"
232          << "# -----------------  ----------------------------------------------------\n"
233          << "# ID:                " << task.id                                                 << '\n'
234          << "# UUID:              " << task.get ("uuid")                                       << '\n'
235          << "# Status:            " << Lexer::ucFirst (Task::statusToText (task.getStatus ())) << '\n'
236          << "# Mask:              " << task.get ("mask")                                       << '\n'
237          << "# iMask:             " << task.get ("imask")                                      << '\n'
238          << "  Project:           " << task.get ("project")                                    << '\n';
239 
240   if (verbose)
241     before << "# Separate the tags with spaces, like this: tag1 tag2\n";
242 
243   before << "  Tags:              " << join (" ", task.getTags ())                      << '\n'
244          << "  Description:       " << task.get ("description")                         << '\n'
245          << "  Created:           " << formatDate (task, "entry", dateformat)           << '\n'
246          << "  Started:           " << formatDate (task, "start", dateformat)           << '\n'
247          << "  Ended:             " << formatDate (task, "end", dateformat)             << '\n'
248          << "  Scheduled:         " << formatDate (task, "scheduled", dateformat)       << '\n'
249          << "  Due:               " << formatDate (task, "due", dateformat)             << '\n'
250          << "  Until:             " << formatDate (task, "until", dateformat)           << '\n'
251          << "  Recur:             " << task.get ("recur")                               << '\n'
252          << "  Wait until:        " << formatDate (task, "wait", dateformat)            << '\n'
253          << "# Modified:          " << formatDate (task, "modified", dateformat)        << '\n'
254          << "  Parent:            " << task.get ("parent")                              << '\n';
255 
256   if (verbose)
257     before << "# Annotations look like this: <date> -- <text> and there can be any number of them.\n"
258               "# The ' -- ' separator between the date and text field should not be removed.\n"
259               "# Multiline annotations need to be indented up to <date> (" << ANNOTATION_EDIT_MARKER.length () - 1 << " spaces).\n"
260               "# A \"blank slot\" for adding an annotation follows for your convenience.\n";
261 
262   for (auto& anno : task.getAnnotations ())
263   {
264     Datetime dt (strtol (anno.first.substr (11).c_str (), nullptr, 10));
265     before << "  Annotation:        " << dt.toString (dateformat)
266            << " -- "                  << str_replace (anno.second, "\n", ANNOTATION_EDIT_MARKER) << '\n';
267   }
268 
269   Datetime now;
270   before << "  Annotation:        " << now.toString (dateformat) << " -- \n";
271 
272   // Add dependencies here.
273   auto dependencies = task.getDependencyUUIDs ();
274   std::stringstream allDeps;
275   for (unsigned int i = 0; i < dependencies.size (); ++i)
276   {
277     if (i)
278       allDeps << ",";
279 
280     Task t;
281     Context::getContext ().tdb2.get (dependencies[i], t);
282     if (t.getStatus () == Task::pending ||
283         t.getStatus () == Task::waiting)
284       allDeps << t.id;
285     else
286       allDeps << dependencies[i];
287   }
288 
289   if (verbose)
290     before << "# Dependencies should be a comma-separated list of task IDs/UUIDs or ID ranges, with no spaces.\n";
291 
292   before << "  Dependencies:      " << allDeps.str () << '\n';
293 
294   // UDAs
295   std::vector <std::string> udas;
296   for (auto& col : Context::getContext ().columns)
297     if (Context::getContext ().config.get ("uda." + col.first + ".type") != "")
298       udas.push_back (col.first);
299 
300   if (udas.size ())
301   {
302     before << "# User Defined Attributes\n";
303     std::sort (udas.begin (), udas.end ());
304     for (auto& uda : udas)
305     {
306       int pad = 13 - uda.length ();
307       std::string padding = "";
308       if (pad > 0)
309         padding = std::string (pad, ' ');
310 
311       std::string type = Context::getContext ().config.get ("uda." + uda + ".type");
312       if (type == "string" || type == "numeric")
313       {
314         auto value = task.get (uda);
315         if (type == "string")
316           value = json::encode (value);
317         before << "  UDA " << uda << ": " << padding << value << '\n';
318       }
319       else if (type == "date")
320         before << "  UDA " << uda << ": " << padding << formatDate (task, uda, dateformat) << '\n';
321       else if (type == "duration")
322         before << "  UDA " << uda << ": " << padding << formatDuration (task, uda) << '\n';
323     }
324   }
325 
326   // UDA orphans
327   auto orphans = task.getUDAOrphanUUIDs ();
328   if (orphans.size ())
329   {
330     before << "# User Defined Attribute Orphans\n";
331     std::sort (orphans.begin (), orphans.end ());
332     for (auto& orphan : orphans)
333     {
334       int pad = 6 - orphan.length ();
335       std::string padding = "";
336       if (pad > 0)
337         padding = std::string (pad, ' ');
338 
339       before << "  UDA Orphan " << orphan << ": " << padding << task.get (orphan) << '\n';
340     }
341   }
342 
343   before << "# End\n";
344   return before.str ();
345 }
346 
347 ////////////////////////////////////////////////////////////////////////////////
parseTask(Task & task,const std::string & after,const std::string & dateformat)348 void CmdEdit::parseTask (Task& task, const std::string& after, const std::string& dateformat)
349 {
350   // project
351   auto value = findValue (after, "\n  Project:");
352   if (task.get ("project") != value)
353   {
354     if (value != "")
355     {
356       Context::getContext ().footnote ("Project modified.");
357       task.set ("project", value);
358     }
359     else
360     {
361       Context::getContext ().footnote ("Project deleted.");
362       task.remove ("project");
363     }
364   }
365 
366   // tags
367   value = findValue (after, "\n  Tags:");
368   task.remove ("tags");
369   task.setTags (split (value, ' '));
370 
371   // description.
372   value = findMultilineValue (after, "\n  Description:", "\n  Created:");
373   if (task.get ("description") != value)
374   {
375     if (value != "")
376     {
377       Context::getContext ().footnote ("Description modified.");
378       task.set ("description", value);
379     }
380     else
381       throw std::string ("Cannot remove description.");
382   }
383 
384   // entry
385   value = findValue (after, "\n  Created:");
386   if (value != "")
387   {
388     if (value != formatDate (task, "entry", dateformat))
389     {
390       Context::getContext ().footnote ("Creation date modified.");
391       task.set ("entry", Datetime (value, dateformat).toEpochString ());
392     }
393   }
394   else
395     throw std::string ("Cannot remove creation date.");
396 
397   // start
398   value = findValue (after, "\n  Started:");
399   if (value != "")
400   {
401     if (task.get ("start") != "")
402     {
403       if (value != formatDate (task, "start", dateformat))
404       {
405         Context::getContext ().footnote (STRING_EDIT_START_MOD);
406         task.set ("start", Datetime (value, dateformat).toEpochString ());
407       }
408     }
409     else
410     {
411       Context::getContext ().footnote (STRING_EDIT_START_MOD);
412       task.set ("start", Datetime (value, dateformat).toEpochString ());
413     }
414   }
415   else
416   {
417     if (task.get ("start") != "")
418     {
419       Context::getContext ().footnote ("Start date removed.");
420       task.remove ("start");
421     }
422   }
423 
424   // end
425   value = findValue (after, "\n  Ended:");
426   if (value != "")
427   {
428     if (task.get ("end") != "")
429     {
430       if (value != formatDate (task, "end", dateformat))
431       {
432         Context::getContext ().footnote ("End date modified.");
433         task.set ("end", Datetime (value, dateformat).toEpochString ());
434       }
435     }
436     else if (task.getStatus () != Task::deleted)
437       throw std::string ("Cannot set a done date on a pending task.");
438   }
439   else
440   {
441     if (task.get ("end") != "")
442     {
443       Context::getContext ().footnote ("End date removed.");
444       task.setStatus (Task::pending);
445       task.remove ("end");
446     }
447   }
448 
449   // scheduled
450   value = findValue (after, "\n  Scheduled:");
451   if (value != "")
452   {
453     if (task.get ("scheduled") != "")
454     {
455       if (value != formatDate (task, "scheduled", dateformat))
456       {
457         Context::getContext ().footnote (STRING_EDIT_SCHED_MOD);
458         task.set ("scheduled", Datetime (value, dateformat).toEpochString ());
459       }
460     }
461     else
462     {
463       Context::getContext ().footnote (STRING_EDIT_SCHED_MOD);
464       task.set ("scheduled", Datetime (value, dateformat).toEpochString ());
465     }
466   }
467   else
468   {
469     if (task.get ("scheduled") != "")
470     {
471       Context::getContext ().footnote ("Scheduled date removed.");
472       task.remove ("scheduled");
473     }
474   }
475 
476   // due
477   value = findValue (after, "\n  Due:");
478   if (value != "")
479   {
480     if (task.get ("due") != "")
481     {
482       if (value != formatDate (task, "due", dateformat))
483       {
484         Context::getContext ().footnote (STRING_EDIT_DUE_MOD);
485         task.set ("due", Datetime (value, dateformat).toEpochString ());
486       }
487     }
488     else
489     {
490       Context::getContext ().footnote (STRING_EDIT_DUE_MOD);
491       task.set ("due", Datetime (value, dateformat).toEpochString ());
492     }
493   }
494   else
495   {
496     if (task.get ("due") != "")
497     {
498       if (task.getStatus () == Task::recurring ||
499           task.get ("parent") != "")
500       {
501         Context::getContext ().footnote ("Cannot remove a due date from a recurring task.");
502       }
503       else
504       {
505         Context::getContext ().footnote ("Due date removed.");
506         task.remove ("due");
507       }
508     }
509   }
510 
511   // until
512   value = findValue (after, "\n  Until:");
513   if (value != "")
514   {
515     if (task.get ("until") != "")
516     {
517       if (value != formatDate (task, "until", dateformat))
518       {
519         Context::getContext ().footnote (STRING_EDIT_UNTIL_MOD);
520         task.set ("until", Datetime (value, dateformat).toEpochString ());
521       }
522     }
523     else
524     {
525       Context::getContext ().footnote (STRING_EDIT_UNTIL_MOD);
526       task.set ("until", Datetime (value, dateformat).toEpochString ());
527     }
528   }
529   else
530   {
531     if (task.get ("until") != "")
532     {
533       Context::getContext ().footnote ("Until date removed.");
534       task.remove ("until");
535     }
536   }
537 
538   // recur
539   value = findValue (after, "\n  Recur:");
540   if (value != task.get ("recur"))
541   {
542     if (value != "")
543     {
544       Duration p;
545       std::string::size_type idx = 0;
546       if (p.parse (value, idx))
547       {
548         Context::getContext ().footnote ("Recurrence modified.");
549         if (task.get ("due") != "")
550         {
551           task.set ("recur", value);
552           task.setStatus (Task::recurring);
553         }
554         else
555           throw std::string ("A recurring task must have a due date.");
556       }
557       else
558         throw std::string ("Not a valid recurrence duration.");
559     }
560     else
561     {
562       Context::getContext ().footnote ("Recurrence removed.");
563       task.setStatus (Task::pending);
564       task.remove ("recur");
565       task.remove ("until");
566       task.remove ("mask");
567       task.remove ("imask");
568     }
569   }
570 
571   // wait
572   value = findValue (after, "\n  Wait until:");
573   if (value != "")
574   {
575     if (task.get ("wait") != "")
576     {
577       if (value != formatDate (task, "wait", dateformat))
578       {
579         Context::getContext ().footnote (STRING_EDIT_WAIT_MOD);
580         task.set ("wait", Datetime (value, dateformat).toEpochString ());
581         task.setStatus (Task::waiting);
582       }
583     }
584     else
585     {
586       Context::getContext ().footnote (STRING_EDIT_WAIT_MOD);
587       task.set ("wait", Datetime (value, dateformat).toEpochString ());
588       task.setStatus (Task::waiting);
589     }
590   }
591   else
592   {
593     if (task.get ("wait") != "")
594     {
595       Context::getContext ().footnote ("Wait date removed.");
596       task.remove ("wait");
597       task.setStatus (Task::pending);
598     }
599   }
600 
601   // parent
602   value = findValue (after, "\n  Parent:");
603   if (value != task.get ("parent"))
604   {
605     if (value != "")
606     {
607       Context::getContext ().footnote ("Parent UUID modified.");
608       task.set ("parent", value);
609     }
610     else
611     {
612       Context::getContext ().footnote ("Parent UUID removed.");
613       task.remove ("parent");
614     }
615   }
616 
617   // Annotations
618   std::map <std::string, std::string> annotations;
619   std::string::size_type found = 0;
620   while ((found = after.find ("\n  Annotation:", found)) != std::string::npos)
621   {
622     found += 14;  // Length of "\n  Annotation:".
623 
624     auto eol = found;
625     while ((eol = after.find ('\n', ++eol)) != std::string::npos)
626       if (after.substr (eol, ANNOTATION_EDIT_MARKER.length ()) != ANNOTATION_EDIT_MARKER)
627         break;
628 
629     if (eol != std::string::npos)
630     {
631       auto value = Lexer::trim (str_replace (after.substr (found, eol - found), ANNOTATION_EDIT_MARKER, "\n"), "\t ");
632       auto gap = value.find (" -- ");
633       if (gap != std::string::npos)
634       {
635         // TODO keeping the initial dates even if dateformat approximates them
636         // is complex as finding the correspondence between each original line
637         // and edited line may be impossible (bug #705). It would be simpler if
638         // each annotation was put on a line with a distinguishable id (then
639         // for each line: if the annotation is the same, then it is copied; if
640         // the annotation is modified, then its original date may be kept; and
641         // if there is no corresponding id, then a new unique date is created).
642         Datetime when (value.substr (0, gap), dateformat);
643 
644         // If the map already contains an annotation for a given timestamp
645         // we need to increment until we find an unused key
646         int timestamp = (int) when.toEpoch ();
647 
648         std::stringstream name;
649 
650         do
651         {
652           name.str ("");  // Clear
653           name << "annotation_" << timestamp;
654           timestamp++;
655         }
656         while (annotations.find (name.str ()) != annotations.end ());
657 
658         auto text = Lexer::trim (value.substr (gap + 4), "\t ");
659         annotations.emplace (name.str (), text);
660       }
661     }
662   }
663 
664   task.setAnnotations (annotations);
665 
666   // Dependencies
667   value = findValue (after, "\n  Dependencies:");
668   auto dependencies = split (value, ',');
669 
670   for (auto& dep : task.getDependencyUUIDs ())
671     task.removeDependency (dep);
672   for (auto& dep : dependencies)
673   {
674     if (dep.length () >= 7)
675       task.addDependency (dep);
676     else
677       task.addDependency ((int) strtol (dep.c_str (), nullptr, 10));
678   }
679 
680   // UDAs
681   for (auto& col : Context::getContext ().columns)
682   {
683     auto type = Context::getContext ().config.get ("uda." + col.first + ".type");
684     if (type != "")
685     {
686       auto value = findValue (after, "\n  UDA " + col.first + ":");
687       if (type == "string")
688         value = json::decode (value);
689       if ((task.get (col.first) != value) && (type != "date" ||
690            (task.get (col.first) != Datetime (value, dateformat).toEpochString ())) &&
691            (type != "duration" ||
692            (task.get (col.first) != Duration (value).toString ())))
693       {
694         if (value != "")
695         {
696           Context::getContext ().footnote (format ("UDA {1} modified.", col.first));
697 
698           if (type == "string")
699           {
700             task.set (col.first, value);
701           }
702           else if (type == "numeric")
703           {
704             Pig pig (value);
705             double d;
706             if (pig.getNumber (d) &&
707                 pig.eos ())
708               task.set (col.first, value);
709             else
710               throw format ("The value '{1}' is not a valid numeric value.", value);
711           }
712           else if (type == "date")
713           {
714             task.set (col.first, Datetime (value, dateformat).toEpochString ());
715           }
716           else if (type == "duration")
717           {
718             task.set (col.first, Duration (value).toTime_t ());
719           }
720         }
721         else
722         {
723           Context::getContext ().footnote (format ("UDA {1} deleted.", col.first));
724           task.remove (col.first);
725         }
726       }
727     }
728   }
729 
730   // UDA orphans
731   for (auto& orphan : findValues (after, "\n  UDA Orphan "))
732   {
733     auto colon = orphan.find (':');
734     if (colon != std::string::npos)
735     {
736       std::string name  = Lexer::trim (orphan.substr (0, colon),  "\t ");
737       std::string value = Lexer::trim (orphan.substr (colon + 1), "\t ");
738 
739       if (value != "")
740         task.set (name, value);
741       else
742         task.remove (name);
743     }
744   }
745 }
746 
747 ////////////////////////////////////////////////////////////////////////////////
editFile(Task & task)748 CmdEdit::editResult CmdEdit::editFile (Task& task)
749 {
750   // Check for file permissions.
751   Directory location (Context::getContext ().config.get ("data.location"));
752   if (! location.writable ())
753     throw std::string ("Your data.location directory is not writable.");
754 
755   // Create a temp file name in data.location.
756   std::stringstream file;
757   file << "task." << task.get ("uuid").substr (0, 8) << ".task";
758 
759   // Determine the output date format, which uses a hierarchy of definitions.
760   //   rc.dateformat.edit
761   //   rc.dateformat
762   auto dateformat = Context::getContext ().config.get ("dateformat.edit");
763   if (dateformat == "")
764     dateformat = Context::getContext ().config.get ("dateformat");
765 
766   // Change directory for the editor
767   auto current_dir = Directory::cwd ();
768   int ignored = chdir (location._data.c_str ());
769   ++ignored; // Keep compiler quiet.
770 
771   // Check if the file already exists, if so, bail out
772   Path filepath = Path (file.str ());
773   if (filepath.exists ())
774     throw std::string ("Task is already being edited.");
775 
776   // Format the contents, T -> text, write to a file.
777   auto before = formatTask (task, dateformat);
778   auto before_orig = before;
779   File::write (file.str (), before);
780 
781   // Determine correct editor: .taskrc:editor > $VISUAL > $EDITOR > vi
782   auto editor = Context::getContext ().config.get ("editor");
783   char* peditor = getenv ("VISUAL");
784   if (editor == "" && peditor) editor = std::string (peditor);
785   peditor = getenv ("EDITOR");
786   if (editor == "" && peditor) editor = std::string (peditor);
787   if (editor == "") editor = "vi";
788 
789   // Complete the command line.
790   editor += ' ';
791   editor += '"' + file.str () + '"';
792 
793 ARE_THESE_REALLY_HARMFUL:
794   bool changes = false; // No changes made.
795 
796   // Launch the editor.
797   std::cout << format ("Launching '{1}' now...\n", editor);
798   int exitcode = system (editor.c_str ());
799   auto captured_errno = errno;
800   if (0 == exitcode)
801     std::cout << "Editing complete.\n";
802   else
803   {
804     std::cout << format ("Editing failed with exit code {1}.\n", exitcode);
805     if (-1 == exitcode)
806       std::cout << std::strerror (captured_errno) << '\n';
807     File::remove (file.str ());
808     return CmdEdit::editResult::error;
809   }
810 
811   // Slurp file.
812   std::string after;
813   File::read (file.str (), after);
814 
815   // Update task based on what can be parsed back out of the file, but only
816   // if changes were made.
817   if (before_orig != after)
818   {
819     std::cout << "Edits were detected.\n";
820     std::string problem = "";
821     auto oops = false;
822 
823     try
824     {
825       parseTask (task, after, dateformat);
826     }
827 
828     catch (const std::string& e)
829     {
830       problem = e;
831       oops = true;
832     }
833 
834     if (oops)
835     {
836       std::cerr << "Error: " << problem << '\n';
837 
838       File::remove (file.str());
839 
840       if (confirm ("Taskwarrior couldn't handle your edits.  Would you like to try again?"))
841       {
842         // Preserve the edits.
843         before = after;
844         File::write (file.str (), before);
845 
846         goto ARE_THESE_REALLY_HARMFUL;
847       }
848     }
849     else
850       changes = true;
851   }
852   else
853   {
854     std::cout << "No edits were detected.\n";
855     changes = false;
856   }
857 
858   // Cleanup.
859   File::remove (file.str ());
860   ignored = chdir (current_dir.c_str ());
861   return changes
862          ? CmdEdit::editResult::changes
863          : CmdEdit::editResult::nochanges;
864 }
865 
866 ////////////////////////////////////////////////////////////////////////////////
867