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