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 <CmdNews.h>
29 #include <iostream>
30 #include <cmath>
31 #include <csignal>
32 #include <Table.h>
33 #include <Context.h>
34 #include <Datetime.h>
35 #include <Duration.h>
36 #include <shared.h>
37 #include <format.h>
38 #include <util.h>
39 #include <main.h>
40 
41 ////////////////////////////////////////////////////////////////////////////////
CmdNews()42 CmdNews::CmdNews ()
43 {
44   _keyword               = "news";
45   _usage                 = "task          news";
46   _description           = "Displays news about the recent releases";
47   _read_only             = true;
48   _displays_id           = false;
49   _needs_gc              = false;
50   _uses_context          = false;
51   _accepts_filter        = false;
52   _accepts_modifications = false;
53   _accepts_miscellaneous = true;
54   _category              = Command::Category::misc;
55 }
56 
57 ////////////////////////////////////////////////////////////////////////////////
signal_handler(int s)58 static void signal_handler (int s)
59 {
60   if (s == SIGINT)
61   {
62     Color footnote;
63     if (Context::getContext ().color ()) {
64       if (Context::getContext ().config.has ("color.footnote"))
65         footnote = Color (Context::getContext ().config.get ("color.footnote"));
66     }
67 
68     std::cout << "\n\nCome back and read about new features later!\n";
69 
70     std::cout << footnote.colorize (
71       "\nIf you enjoy Taskwarrior, please consider supporting the project at:\n"
72       "    https://github.com/sponsors/GothenburgBitFactory/\n"
73     );
74     exit (1);
75   }
76 }
77 
wait_for_enter()78 void wait_for_enter ()
79 {
80   signal (SIGINT, signal_handler);
81 
82   std::string dummy;
83   std::cout << "\nPress enter to continue..";
84   std::getline (std::cin, dummy);
85   std::cout << "\33[2K\033[A\33[2K";  // Erase current line, move up, and erase again
86 
87   signal (SIGINT, SIG_DFL);
88 }
89 
90 ////////////////////////////////////////////////////////////////////////////////
91 // Holds information about single improvement / bug.
92 //
NewsItem(bool major,const std::string & title,const std::string & bg_title,const std::string & background,const std::string & punchline,const std::string & update,const std::string & reasoning,const std::string & actions)93 NewsItem::NewsItem (
94   bool major,
95   const std::string& title,
96   const std::string& bg_title,
97   const std::string& background,
98   const std::string& punchline,
99   const std::string& update,
100   const std::string& reasoning,
101   const std::string& actions
102 ) {
103   _major = major;
104   _title = title;
105   _bg_title = bg_title;
106   _background = background;
107   _punchline = punchline;
108   _update = update;
109   _reasoning = reasoning;
110   _actions = actions;
111 }
112 
render()113 void NewsItem::render () {
114   auto config = Context::getContext ().config;
115   Color header;
116   Color footnote;
117   Color bold;
118   Color underline;
119   if (Context::getContext ().color ()) {
120     bold = Color ("bold");
121     underline = Color ("underline");
122     if (config.has ("color.header"))
123       header = Color (config.get ("color.header"));
124     if (config.has ("color.footnote"))
125       footnote = Color (config.get ("color.footnote"));
126   }
127 
128   // TODO: For some reason, bold cannot be blended in 256-color terminals
129   // Apply this workaround of colorizing twice.
130   std::cout << bold.colorize (header.colorize (format ("{1}\n", _title)));
131   if (_background.size ()) {
132     if (_bg_title.empty ())
133       _bg_title = "Background";
134 
135     std::cout << "\n  " << underline.colorize (_bg_title) << std::endl
136               << _background << std::endl;
137   }
138 
139   wait_for_enter ();
140 
141   std::cout << "  " << underline.colorize ("What changed in 2.6.0?\n");
142   if (_punchline.size ())
143     std::cout << footnote.colorize (format ("{1}\n", _punchline));
144 
145   if (_update.size ())
146     std::cout << format ("{1}\n", _update);
147 
148   wait_for_enter ();
149 
150   if (_reasoning.size ()) {
151     std::cout << "  " << underline.colorize ("What was the motivation behind this feature?\n")
152               << _reasoning << std::endl;
153     wait_for_enter ();
154   }
155 
156   if (_actions.size ()) {
157     std::cout << "  " << underline.colorize ("What do I have to do?\n")
158               << _actions << std::endl;
159     wait_for_enter ();
160   }
161 }
162 
163 ////////////////////////////////////////////////////////////////////////////////
164 // Generate the highlights for the 2.6.0 version.
165 //
166 // - XDG directory mode (high)
167 // - Support for Unicode 11 characters (high)
168 // - 64 bit values, UDAs, Datetime values until year 9999 (high)
169 // - Config context variables
170 // - Reports outside of context
171 // - Environment variables in taskrc (high)
172 // - Waiting is a virtual concept (high)
173 // - Improved parser and task display mechanism
174 // - The .by attribute modifier
175 // - Exporting a report
176 // - Multi-day holidays
version2_6_0(std::vector<NewsItem> & items)177 void CmdNews::version2_6_0 (std::vector<NewsItem>& items) {
178   /////////////////////////////////////////////////////////////////////////////
179   // - Writeable context (major)
180 
181   // Detect whether user uses any contexts
182   auto config = Context::getContext ().config;
183   std::stringstream advice;
184 
185   auto defined = CmdContext::getContexts ();
186   if (defined.size ())
187   {
188     // Detect the old-style contexts
189     std::vector<std::string> old_style;
190     std::copy_if (
191       defined.begin(),
192       defined.end(),
193       std::back_inserter(old_style),
194       [&](auto& name){return config.has ("context." + name);}
195     );
196 
197     if (old_style.size ())
198     {
199       advice << format (
200         "  You have {1} defined contexts, out of which {2} are old-style:\n",
201         defined.size (),
202         std::count_if (
203           defined.begin (),
204           defined.end (),
205           [&](auto& name){return config.has ("context." + name);}
206       ));
207 
208       for (auto context: defined) {
209         std::string old_definition = config.get ("context." + context);
210         if (old_definition != "")
211           advice << format ("  * {1}: {2}\n", context, old_definition);
212       }
213 
214       advice << "\n"
215                 "  These need to be migrated to new-style, which uses context.<name>.read and\n"
216                 "  context.<name>.write config variables. Please run the following commands:\n";
217 
218       for (auto context: defined) {
219         std::string old_definition = config.get ("context." + context);
220         if (old_definition != "")
221           advice << format ("  $ task context define {1} '{2}'\n", context, old_definition);
222       }
223 
224       advice << "\n"
225                 "  Please check these filters are also valid modifications. If a context filter is not\n"
226                 "  a valid modification, you can set the context.<name>.write configuration variable to\n"
227                 "  specify the write context explicitly. Read more in CONTEXT section of man taskrc.";
228     }
229     else
230       advice << "  You don't have any old-style contexts defined, so you're good to go as is!";
231   }
232   else
233     advice << "  You don't have any contexts defined, so you're good to go as is!\n"
234               "  Read more about how to use contexts in CONTEXT section of 'man task'.";
235 
236   NewsItem writeable_context (
237     true,
238     "'Writeable' context",
239     "Background - what is context?",
240     "  The 'context' is a feature (introduced in 2.5.0) that allows users to apply a\n"
241     "  predefined filter to all task reports.\n"
242     "  \n"
243     "    $ task context define work \"project:Work or +urgent\"\n"
244     "    $ task context work\n"
245     "    Context 'work' set. Use 'task context none' to remove.\n"
246     "  \n"
247     "  Now if we proceed to add two tasks:\n"
248     "    $ task add Talk to Jeff pro:Work\n"
249     "    $ task add Call mom pro:Personal\n"
250     "  \n"
251     "    $ task\n"
252     "    ID Age   Project Description  Urg\n"
253     "     1 16s   Work    Talk to Jeff    1\n"
254     "  \n"
255     "  The task \"Call mom\" will not be listed, because it does not match\n"
256     "  the active context (its project is 'Personal' and not 'Work').",
257     "  The currently active context definition is now applied as default modifications\n"
258     "  when creating new tasks using 'task add' and 'task log'.",
259     "  \n"
260     "  Consider following example, using contex 'work' defined as 'project:Work' above:\n"
261     "  \n"
262     "    $ task context work\n"
263     "    $ task add Talk to Jeff\n"
264     "    $ task\n"
265     "    ID Age  Project Description  Urg \n"
266     "     1 1s   Work    Talk to Jeff    1\n"
267     "            ^^^^^^^\n"
268     "  \n"
269     "  Note that project attribute was set to 'Work' automatically.",
270     "  This was a popular feature request. Now, if you have a context active,\n"
271     "  newly added tasks no longer \"fall outside\" of the context by default.",
272     advice.str ()
273   );
274   items.push_back(writeable_context);
275 
276   /////////////////////////////////////////////////////////////////////////////
277   // - 64-bit datetime support (major)
278 
279   NewsItem uint64_support (
280     false,
281     "Support for 64-bit timestamps and numeric values",
282     "",
283     "",
284     "  Taskwarrior now supports 64-bit timestamps, making it possible to set due dates\n"
285     "  and other date attributes beyond 19 January 2038 (limit of 32-bit timestamps).\n",
286     "  The current limit is 31 December 9999 for display reasons (last 4-digit year).",
287     "  With each year passing by faster than the last, setting tasks for 2040s\n"
288     "  is not as unfeasible as it once was.",
289     "  Don't forget that 50-year anniversary and 'task add' a long-term task today!"
290   );
291   items.push_back(uint64_support);
292 
293   /////////////////////////////////////////////////////////////////////////////
294   // - Waiting is a virtual status
295 
296   NewsItem waiting_status (
297     true,
298     "Deprecation of the status:waiting",
299     "",
300     "  If a task has a 'wait' attribute set to a date in the future, it is modified\n"
301     "  to have a 'waiting' status. Once that date is no longer in the future, the status\n"
302     "  is modified to back to 'pending'.",
303     "  The 'waiting' value of status is deprecated, instead users should use +WAITING\n"
304     "  virtual tag, or explicitly query for wait.after:now (the two are equivalent).",
305     "  \n"
306     "  The status:waiting query still works in 2.6.0, but support will be dropped in 3.0.",
307     "",
308     "  In your custom report definitions, the following expressions should be replaced:\n"
309     "  * 'status:pending or status:waiting' should be replaced by 'status:pending'\n"
310     "  * 'status:pending' should be replaced by 'status:pending -WAITING'"
311   );
312   items.push_back(waiting_status);
313 
314   /////////////////////////////////////////////////////////////////////////////
315   // - Support for environment variables in the taskrc
316 
317   NewsItem env_vars (
318     true,
319     "Environment variables in the taskrc",
320     "",
321     "",
322     "  Taskwarrior now supports expanding environment variables in the taskrc file,\n"
323     "  allowing users to customize the behaviour of 'task' based on the current env.\n",
324     "  The environment variables can either be used in paths, or as separate values:\n"
325     "    data.location=$XDG_DATA_HOME/task/\n"
326     "    default.project=$PROJECT",
327     "",
328     ""
329   );
330   items.push_back(env_vars);
331 
332   /////////////////////////////////////////////////////////////////////////////
333   // - Reports outside of context
334 
335   NewsItem contextless_reports (
336     true,
337     "Context-less reports",
338     "",
339     "  By default, every report is affected by currently active context.",
340     "  You can now make a selected report ignore currently active context by setting\n"
341     "  'report.<name>.context' configuration variable to 0.",
342     "",
343     "  This is useful for users who utilize a single place (such as project:Inbox)\n"
344     "  to collect their new tasks that are then triaged on a regular basis\n"
345     "  (such as in GTD methodology).\n"
346     "  \n"
347     "  In such a case, defining a report that filters for project:Inbox and making it\n"
348     "  fully accessible from any context is a major usability improvement.",
349     ""
350   );
351   items.push_back(contextless_reports);
352 
353   /////////////////////////////////////////////////////////////////////////////
354   // - Exporting a particular report
355 
356   NewsItem exportable_reports (
357     false,
358     "Exporting a particular report",
359     "",
360     "",
361     "  You can now export the tasks listed by a particular report as JSON by simply\n"
362     "  calling 'task export <report>'.\n",
363     "  The export mirrors the filter and the sort order of the report.",
364     "  This feature can be used to quickly process the data displayed in a particular\n"
365     "  report using other CLI tools. For example, the following oneliner\n"
366     "  \n"
367     "      $ task export next | jq '.[].urgency' | datamash mean 1\n"
368     "      3.3455535142857\n"
369     "  \n"
370     "  combines jq and GNU datamash to compute average urgency of the tasks displayed\n"
371     "  in the 'next' report.",
372     ""
373   );
374   items.push_back(exportable_reports);
375 
376   /////////////////////////////////////////////////////////////////////////////
377   // - Multi-day holidays
378 
379   NewsItem multi_holidays (
380     false,
381     "Multi-day holidays",
382     "",
383     "  Holidays are currently used in 'task calendar' to visualize the workload during\n"
384     "  the upcoming weeks/months. Up to date country-specific holiday data files can be\n"
385     "  obtained from our website, holidata.net.",
386     "  Instead of single-day holiday entries only, Taskwarrior now supports holidays\n"
387     "  that span a range of days (i.e. vacation).\n",
388     "  Use a holday.<name>.start and holiday.<name>.end to configure a multi-day holiday:\n"
389     "  \n"
390     "      holiday.sysadmin.name=System Administrator Appreciation Week\n"
391     "      holiday.sysadmin.start=20100730\n"
392     "      holiday.sysadmin.end=20100805",
393     "",
394     ""
395   );
396   items.push_back(multi_holidays);
397 
398   /////////////////////////////////////////////////////////////////////////////
399   // - Unicode 12
400 
401   NewsItem unicode_12 (
402     false,
403     "Extended Unicode support (Unicode 12)",
404     "",
405     "",
406     "  The support for Unicode character set was improved to support Unicode 12.\n"
407     "  This means better support for various language-specific characters - and emojis!",
408     "",
409     "  Extended unicode support for language specific characters helps non-English users.\n"
410     "  While most users don't enter emojis as task metadata, automated task creation tools,\n"
411     "  such as bugwarrior, might create tasks with exotic Unicode data.",
412     "  You can try it out - 'task add Prepare for an �� invasion!'"
413   );
414   items.push_back(unicode_12);
415 
416   /////////////////////////////////////////////////////////////////////////////
417   // - The .by attribute modifier
418 
419   NewsItem by_modifier (
420     false,
421     "The .by attribute modifier",
422     "",
423     "",
424     "  A new attribute modifier '.by' was introduced, equivalent to the operator '<='.\n",
425     "  This modifier can be used to list all tasks due by the end of the months,\n"
426     "  including the last day of the month, using: 'due.by:eom' query",
427     "  There was no convenient way to express '<=' relation using attribute modifiers.\n"
428     "  As a workaround, instead of 'due.by:eom' one could use 'due.before:eom+1d',\n"
429     "  but that requires a certain amount of mental overhead.",
430     ""
431   );
432   items.push_back(by_modifier);
433 
434   /////////////////////////////////////////////////////////////////////////////
435   // - Context-specific configuration overrides
436 
437   NewsItem context_config (
438     false,
439     "Context-specific configuration overrides",
440     "",
441     "",
442     "  Any context can now define context-specific configuration overrides\n"
443     "  via context.<name>.rc.<setting>=<value>.\n",
444     "  This allows the user to customize the behaviour of Taskwarrior in a given context,\n"
445     "  for example, to change the default command in the 'work' context to 'overdue':\n"
446     "\n"
447     "      $ task config context.work.rc.default.command overdue\n"
448     "\n"
449     "  Another example would be to ensure that while context 'work' is active, tasks get\n"
450     "  stored in a ~/.worktasks directory:\n"
451     "\n"
452     "      $ task config context.work.rc.data.location=~/.worktasks",
453     "",
454     ""
455   );
456   items.push_back(context_config);
457 
458   /////////////////////////////////////////////////////////////////////////////
459   // - XDG config home support
460 
461   NewsItem xdg_support (
462     true,
463     "Support for XDG Base Directory Specification",
464     "",
465     "  The XDG Base Directory specification provides standard locations to store\n"
466     "  application data, configuration, state, and cached data in order to keep $HOME\n"
467     "  clutter-free. The locations are usually set to ~/.local/share, ~/.config,\n"
468     "  ~/.local/state and ~/.cache respectively.",
469     "  If taskrc is not found at '~/.taskrc', Taskwarrior will attempt to find it\n"
470     "  at '$XDG_CONFIG_HOME/task/taskrc' (defaults to '~/.config/task/taskrc').",
471     "",
472     "  This allows users to fully follow XDG Base Directory Spec by moving their taskrc:\n"
473     "      $ mkdir $XDG_CONFIG_HOME/task\n"
474     "      $ mv ~/.taskrc $XDG_CONFIG_HOME/task/taskrc\n\n"
475     "  and further setting:\n"
476     "      data.location=$XDG_DATA_HOME/task/\n"
477     "      hooks.location=$XDG_CONFIG_HOME/task/hooks/\n\n"
478     "  Solutions in the past required symlinks or more cumbersome configuration overrides.",
479     "  If you configure your data.location and hooks.location as above, ensure\n"
480     "  that the XFG_DATA_HOME and XDG_CONFIG_HOME environment variables are set,\n"
481     "  otherwise they're going to expand to empty string. Alternatively you can\n"
482     "  hardcode the desired paths on your system."
483   );
484   items.push_back(xdg_support);
485 
486   /////////////////////////////////////////////////////////////////////////////
487   // - Update holiday data
488 
489   NewsItem holidata_2022 (
490     false,
491     "Updated holiday data for 2022",
492     "",
493     "",
494     "  Holiday data has been refreshed for 2022 and five more holiday locales\n"
495     "  have been added: fr-CA, hu-HU, pt-BR, sk-SK and sv-FI.",
496     "",
497     "  Refreshing the holiday data is part of every release. The addition of the new\n"
498     "  locales allows us to better support users in those particular countries."
499   );
500   items.push_back(holidata_2022);
501 }
502 
503 ////////////////////////////////////////////////////////////////////////////////
execute(std::string & output)504 int CmdNews::execute (std::string& output)
505 {
506   auto words = Context::getContext ().cli2.getWords ();
507   auto config = Context::getContext ().config;
508 
509   // Supress compiler warning about unused argument
510   output = "";
511 
512   // TODO: 2.6.0 is the only version with explicit release notes, but in the
513   // future we need to only execute yet unread release notes
514   std::vector<NewsItem> items;
515   std::string version = "2.6.0";
516   version2_6_0 (items);
517 
518   bool full_summary = false;
519   bool major_items = true;
520 
521   for (auto word: words)
522   {
523     if (word == "major") {
524         major_items = true;
525         break;
526     }
527 
528     if (word == "minor") {
529         major_items = false;
530         break;
531     }
532 
533     if (word == "all") {
534         full_summary = true;
535         break;
536     }
537   }
538 
539   signal (SIGINT, signal_handler);
540 
541   // Remove non-major items if displaying a non-full (abbreviated) summary
542   int total_highlights = items.size ();
543   if (! full_summary)
544     items.erase (
545       std::remove_if (items.begin (), items.end (), [&](const NewsItem& n){return n._major != major_items;}),
546       items.end ()
547     );
548 
549   // Print release notes
550   Color bold = Color ("bold");
551   std::cout << bold.colorize (format (
552     "\n"
553     "==========================================\n"
554     "Taskwarrior {1} {2} Release highlights\n"
555     "==========================================\n",
556     version,
557     (full_summary ? "All" : (major_items ? "Major" : "Minor"))
558    ));
559 
560   for (unsigned short i=0; i < items.size (); i++) {
561     std::cout << format ("\n({1}/{2}) ", i+1, items.size ());
562     items[i].render ();
563   }
564 
565   std::cout << "Thank you for catching up on the new features!\n";
566   wait_for_enter ();
567 
568   // Display outro
569   Datetime now;
570   Datetime beginning (2006, 11, 29);
571   Duration development_time = Duration (now - beginning);
572 
573   Color underline = Color ("underline");
574 
575   std::stringstream outro;
576   outro << underline.colorize (bold.colorize ("Taskwarrior crowdfunding\n"));
577   outro << format (
578     "Taskwarrior has been in development for {1} years but its survival\n"
579     "depends on your support!\n\n"
580     "Please consider joining our {2} fundraiser to help us fund maintenance\n"
581     "and development of new features:\n\n",
582     std::lround (static_cast<float>(development_time.days ()) / 365.25),
583     now.year ()
584   );
585   outro << bold.colorize("    https://github.com/sponsors/GothenburgBitFactory/\n\n");
586   outro << "Perks are available for our sponsors.\n";
587 
588   std::cout << outro.str ();
589 
590   // Set a mark in the config to remember which version's release notes were displayed
591   if (config.get ("news.version") != "2.6.0")
592   {
593     CmdConfig::setConfigVariable ("news.version", "2.6.0", false);
594 
595     // Revert back to default signal handling after displaying the outro
596     signal (SIGINT, SIG_DFL);
597 
598     std::string question = format (
599       "\nWould you like to open Taskwarrior {1} fundraising campaign to read more?",
600       now.year ()
601     );
602 
603     std::vector <std::string> options {"yes", "no"};
604     std::vector <std::string> matches;
605 
606     std::cout << question << " (YES/no) ";
607 
608     std::string answer;
609     std::getline (std::cin, answer);
610 
611     if (std::cin.eof () || trim (answer).empty ())
612       answer = "yes";
613     else
614       lowerCase (trim (answer));
615 
616     autoComplete (answer, options, matches, 1); // Hard-coded 1.
617 
618     if (matches.size () == 1 && matches[0] == "yes")
619       system ("xdg-open 'https://github.com/sponsors/GothenburgBitFactory/'");
620 
621     std::cout << std::endl;
622   }
623   else
624     wait_for_enter ();  // Do not display the outro and footnote at once
625 
626   if (! full_summary && major_items)
627     Context::getContext ().footnote (format (
628       "Only major higlights were displayed ({1} out of {2} total).\n"
629       "If you're interested in more release highlights, run 'task news {3} minor'.",
630       items.size (),
631       total_highlights,
632       version
633     ));
634 
635   return 0;
636 }
637