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